Просмотр исходного кода

Even master with 0.19.0 release

rafalp 6 лет назад
Родитель
Сommit
f43696ca09
394 измененных файлов с 16423 добавлено и 9635 удалено
  1. 1 1
      Dockerfile
  2. 2 2
      MANIFEST.in
  3. 6 16
      README.rst
  4. 75 13
      frontend/src/components/RegisterLegalFootnote.js
  5. 57 0
      frontend/src/components/accept-agreement.js
  6. 1 1
      frontend/src/components/avatar.js
  7. 1 1
      frontend/src/components/options/delete-account.js
  8. 156 0
      frontend/src/components/options/download-data.js
  9. 8 0
      frontend/src/components/options/root.js
  10. 0 15
      frontend/src/components/posts-list/event/info.js
  11. 61 16
      frontend/src/components/register.js
  12. 1 1
      frontend/src/components/social-auth/complete.js
  13. 71 12
      frontend/src/components/social-auth/register.js
  14. 21 0
      frontend/src/initializers/components/accept-agreement.js
  15. 13 3
      frontend/src/utils/validators.js
  16. 5 15
      frontend/style/flavor/modals.less
  17. 1 0
      frontend/style/index.less
  18. 68 0
      frontend/style/misago/agreement-overlay.less
  19. 1 1
      initdev
  20. 1 1
      misago/__init__.py
  21. 1 2
      misago/acl/testutils.py
  22. 0 48
      misago/admin/forms.py
  23. 67 0
      misago/admin/tests/test_admin_index.py
  24. 0 13
      misago/admin/tests/test_admin_views.py
  25. 1 1
      misago/admin/views/auth.py
  26. 13 0
      misago/admin/views/index.py
  27. 0 1
      misago/api/__init__.py
  28. 0 7
      misago/api/apps.py
  29. 0 7
      misago/api/context_processors.py
  30. 0 25
      misago/api/exceptionhandler.py
  31. 0 15
      misago/api/middleware.py
  32. 0 155
      misago/api/patch.py
  33. 0 26
      misago/api/tests/test_context_processors.py
  34. 0 60
      misago/api/tests/test_exceptionhandler.py
  35. 0 171
      misago/api/tests/test_patch.py
  36. 0 201
      misago/api/tests/test_patch_dispatch.py
  37. 0 227
      misago/api/tests/test_patch_dispatch_bulk.py
  38. 0 20
      misago/api/testutils.py
  39. 2 0
      misago/categories/__init__.py
  40. 2 0
      misago/categories/constants.py
  41. 0 27
      misago/categories/context_processors.py
  42. 3 2
      misago/categories/forms.py
  43. 2 4
      misago/categories/migrations/0002_default_categories.py
  44. 5 5
      misago/categories/models.py
  45. 35 22
      misago/categories/serializers.py
  46. 2 2
      misago/categories/signals.py
  47. 3 2
      misago/categories/tests/test_category_model.py
  48. 1 1
      misago/categories/urls/api.py
  49. 3 2
      misago/categories/views/categoriesadmin.py
  50. 3 2
      misago/categories/views/categorieslist.py
  51. 17 23
      misago/conf/context_processors.py
  52. 40 18
      misago/conf/defaults.py
  53. 1 1
      misago/conf/tests/test_context_processors.py
  54. 116 0
      misago/core/apipatch.py
  55. 0 0
      misago/core/apirouter.py
  56. 25 0
      misago/core/context_processors.py
  57. 8 5
      misago/core/errorpages.py
  58. 17 0
      misago/core/exceptionhandler.py
  59. 25 11
      misago/core/mail.py
  60. 7 0
      misago/core/middleware/frontendcontext.py
  61. 0 21
      misago/core/migrations/0003_remove_forum_branding_display_setting.py
  62. 0 0
      misago/core/rest_permissions.py
  63. 0 0
      misago/core/serializers.py
  64. 19 0
      misago/core/templatetags/misago_absoluteurl.py
  65. 1 3
      misago/core/testproject/urls.py
  66. 1 1
      misago/core/testproject/urlswitherrorhandlers.py
  67. 1 20
      misago/core/testproject/views.py
  68. 323 0
      misago/core/tests/test_apipatch.py
  69. 19 0
      misago/core/tests/test_context_processors.py
  70. 7 17
      misago/core/tests/test_errorpages.py
  71. 1 6
      misago/core/tests/test_exceptionhandler_middleware.py
  72. 38 0
      misago/core/tests/test_exceptionhandlers.py
  73. 2 8
      misago/core/tests/test_frontendcontext_middleware.py
  74. 8 9
      misago/core/tests/test_mail.py
  75. 1 1
      misago/core/tests/test_serializers.py
  76. 27 1
      misago/core/tests/test_templatetags.py
  77. 67 1
      misago/core/tests/test_utils.py
  78. 26 11
      misago/core/utils.py
  79. 0 2
      misago/faker/management/commands/createfakethreads.py
  80. 31 0
      misago/legal/admin.py
  81. 29 0
      misago/legal/api.py
  82. 3 0
      misago/legal/apps.py
  83. 44 22
      misago/legal/context_processors.py
  84. 79 0
      misago/legal/forms.py
  85. 50 0
      misago/legal/migrations/0002_agreement_useragreement.py
  86. 84 0
      misago/legal/migrations/0003_create_agreements_from_settings.py
  87. 95 1
      misago/legal/models.py
  88. 16 0
      misago/legal/signals.py
  89. 0 193
      misago/legal/tests.py
  90. 0 0
      misago/legal/tests/__init__.py
  91. 313 0
      misago/legal/tests/test_admin_views.py
  92. 105 0
      misago/legal/tests/test_api.py
  93. 186 0
      misago/legal/tests/test_context_processors.py
  94. 96 0
      misago/legal/tests/test_required_agreement.py
  95. 186 0
      misago/legal/tests/test_utils.py
  96. 84 0
      misago/legal/tests/test_views.py
  97. 2 2
      misago/legal/urls/__init__.py
  98. 8 0
      misago/legal/urls/api.py
  99. 66 0
      misago/legal/utils.py
  100. 0 69
      misago/legal/views.py
  101. 1 0
      misago/legal/views/__init__.py
  102. 87 0
      misago/legal/views/admin.py
  103. 34 0
      misago/legal/views/legal.py
  104. BIN
      misago/locale/en/LC_MESSAGES/django.mo
  105. 719 292
      misago/locale/en/LC_MESSAGES/django.po
  106. BIN
      misago/locale/en/LC_MESSAGES/djangojs.mo
  107. 285 206
      misago/locale/en/LC_MESSAGES/djangojs.po
  108. BIN
      misago/locale/es/LC_MESSAGES/django.mo
  109. 726 303
      misago/locale/es/LC_MESSAGES/django.po
  110. BIN
      misago/locale/es/LC_MESSAGES/djangojs.mo
  111. 285 207
      misago/locale/es/LC_MESSAGES/djangojs.po
  112. BIN
      misago/locale/fr/LC_MESSAGES/django.mo
  113. 726 303
      misago/locale/fr/LC_MESSAGES/django.po
  114. BIN
      misago/locale/fr/LC_MESSAGES/djangojs.mo
  115. 285 207
      misago/locale/fr/LC_MESSAGES/djangojs.po
  116. BIN
      misago/locale/ru/LC_MESSAGES/django.mo
  117. 728 303
      misago/locale/ru/LC_MESSAGES/django.po
  118. BIN
      misago/locale/ru/LC_MESSAGES/djangojs.mo
  119. 285 207
      misago/locale/ru/LC_MESSAGES/djangojs.po
  120. BIN
      misago/locale/tr/LC_MESSAGES/django.mo
  121. 726 303
      misago/locale/tr/LC_MESSAGES/django.po
  122. BIN
      misago/locale/tr/LC_MESSAGES/djangojs.mo
  123. 285 207
      misago/locale/tr/LC_MESSAGES/djangojs.po
  124. BIN
      misago/locale/zh_Hans/LC_MESSAGES/django.mo
  125. 738 311
      misago/locale/zh_Hans/LC_MESSAGES/django.po
  126. BIN
      misago/locale/zh_Hans/LC_MESSAGES/djangojs.mo
  127. 300 222
      misago/locale/zh_Hans/LC_MESSAGES/djangojs.po
  128. 9 13
      misago/markup/api.py
  129. 9 0
      misago/markup/context_processors.py
  130. 11 0
      misago/markup/serializers.py
  131. 26 74
      misago/markup/tests/test_api.py
  132. 4 0
      misago/project_template/cron.txt
  133. 53 20
      misago/project_template/project_name/settings.py
  134. 1 1
      misago/project_template/project_name/urls.py
  135. 3 0
      misago/project_template/userdata/README.txt
  136. 2 2
      misago/readtracker/signals.py
  137. 6 2
      misago/search/context_processors.py
  138. 1 4
      misago/search/tests/test_api.py
  139. 3 0
      misago/search/views.py
  140. 1 1
      misago/static/misago/css/misago.css
  141. 25 24
      misago/static/misago/js/misago.js
  142. 0 0
      misago/static/misago/js/misago.js.map
  143. 68 0
      misago/templates/misago/admin/agreements/form.html
  144. 148 0
      misago/templates/misago/admin/agreements/list.html
  145. 2 2
      misago/templates/misago/admin/attachments/list.html
  146. 38 0
      misago/templates/misago/admin/datadownloads/form.html
  147. 113 0
      misago/templates/misago/admin/datadownloads/list.html
  148. 29 2
      misago/templates/misago/admin/index.html
  149. 5 1
      misago/templates/misago/admin/users/ban.html
  150. 31 0
      misago/templates/misago/admin/users/edit.html
  151. 8 0
      misago/templates/misago/admin/users/list.html
  152. 6 2
      misago/templates/misago/base.html
  153. 3 3
      misago/templates/misago/emails/activation/by_admin.html
  154. 3 3
      misago/templates/misago/emails/activation/by_admin.txt
  155. 3 3
      misago/templates/misago/emails/activation/by_user.html
  156. 3 3
      misago/templates/misago/emails/activation/by_user.txt
  157. 2 1
      misago/templates/misago/emails/base.html
  158. 3 3
      misago/templates/misago/emails/change_email.html
  159. 3 3
      misago/templates/misago/emails/change_email.txt
  160. 3 3
      misago/templates/misago/emails/change_password.html
  161. 3 3
      misago/templates/misago/emails/change_password.txt
  162. 3 3
      misago/templates/misago/emails/change_password_form_link.html
  163. 3 3
      misago/templates/misago/emails/change_password_form_link.txt
  164. 20 0
      misago/templates/misago/emails/data_download.html
  165. 20 0
      misago/templates/misago/emails/data_download.txt
  166. 4 4
      misago/templates/misago/emails/privatethread/added.html
  167. 3 3
      misago/templates/misago/emails/privatethread/added.txt
  168. 3 3
      misago/templates/misago/emails/register/complete.html
  169. 3 3
      misago/templates/misago/emails/register/complete.txt
  170. 3 3
      misago/templates/misago/emails/register/inactive.html
  171. 4 4
      misago/templates/misago/emails/register/inactive.txt
  172. 5 5
      misago/templates/misago/emails/thread/reply.html
  173. 4 4
      misago/templates/misago/emails/thread/reply.txt
  174. 17 17
      misago/templates/misago/errorpages/ban_message.html
  175. 5 5
      misago/templates/misago/footer.html
  176. 1 1
      misago/templates/misago/privacy_policy.html
  177. 35 0
      misago/templates/misago/required_agreement.html
  178. 1 1
      misago/templates/misago/terms_of_service.html
  179. 0 5
      misago/templates/misago/thread/posts/event/info.html
  180. 96 5
      misago/threads/api/attachments.py
  181. 13 12
      misago/threads/api/pollvotecreateendpoint.py
  182. 8 2
      misago/threads/api/postendpoints/delete.py
  183. 1 3
      misago/threads/api/postendpoints/edits.py
  184. 7 1
      misago/threads/api/postendpoints/merge.py
  185. 7 2
      misago/threads/api/postendpoints/move.py
  186. 2 2
      misago/threads/api/postendpoints/patch_event.py
  187. 4 4
      misago/threads/api/postendpoints/patch_post.py
  188. 10 2
      misago/threads/api/postendpoints/split.py
  189. 3 16
      misago/threads/api/postingendpoint/__init__.py
  190. 1 1
      misago/threads/api/postingendpoint/attachments.py
  191. 4 3
      misago/threads/api/postingendpoint/category.py
  192. 2 2
      misago/threads/api/postingendpoint/emailnotification.py
  193. 2 2
      misago/threads/api/postingendpoint/moderationqueue.py
  194. 2 2
      misago/threads/api/postingendpoint/participants.py
  195. 3 2
      misago/threads/api/postingendpoint/privatethread.py
  196. 0 1
      misago/threads/api/postingendpoint/recordedit.py
  197. 3 1
      misago/threads/api/postingendpoint/reply.py
  198. 2 2
      misago/threads/api/postingendpoint/syncprivatethreads.py
  199. 2 2
      misago/threads/api/postingendpoint/updatestats.py
  200. 11 2
      misago/threads/api/threadendpoints/delete.py
  201. 3 2
      misago/threads/api/threadendpoints/editor.py
  202. 1 1
      misago/threads/api/threadendpoints/list.py
  203. 59 17
      misago/threads/api/threadendpoints/merge.py
  204. 28 30
      misago/threads/api/threadendpoints/patch.py
  205. 5 1
      misago/threads/api/threadpoll.py
  206. 4 4
      misago/threads/api/threads.py
  207. 1 1
      misago/threads/checksums.py
  208. 13 0
      misago/threads/context_processors.py
  209. 0 1
      misago/threads/events.py
  210. 7 9
      misago/threads/management/commands/clearattachments.py
  211. 10 13
      misago/threads/management/commands/rebuildpostssearch.py
  212. 3 5
      misago/threads/management/commands/synchronizethreads.py
  213. 37 0
      misago/threads/management/commands/updatepostschecksums.py
  214. 39 0
      misago/threads/migrations/0010_auto_20180609_1523.py
  215. 0 1
      misago/threads/models/attachment.py
  216. 0 1
      misago/threads/models/poll.py
  217. 0 1
      misago/threads/models/pollvote.py
  218. 0 1
      misago/threads/models/post.py
  219. 0 1
      misago/threads/models/postedit.py
  220. 0 1
      misago/threads/models/postlike.py
  221. 1 1
      misago/threads/models/thread.py
  222. 2 5
      misago/threads/moderation/__init__.py
  223. 10 0
      misago/threads/moderation/posts.py
  224. 16 0
      misago/threads/moderation/threads.py
  225. 2 3
      misago/threads/participants.py
  226. 9 2
      misago/threads/permissions/privatethreads.py
  227. 0 52
      misago/threads/pollmergehandler.py
  228. 10 13
      misago/threads/serializers/__init__.py
  229. 30 108
      misago/threads/serializers/attachment.py
  230. 7 2
      misago/threads/serializers/feed.py
  231. 43 66
      misago/threads/serializers/moderation.py
  232. 42 8
      misago/threads/serializers/poll.py
  233. 19 11
      misago/threads/serializers/pollvote.py
  234. 60 9
      misago/threads/serializers/post.py
  235. 24 2
      misago/threads/serializers/postedit.py
  236. 21 6
      misago/threads/serializers/postlike.py
  237. 91 32
      misago/threads/serializers/thread.py
  238. 9 5
      misago/threads/serializers/threadparticipant.py
  239. 64 22
      misago/threads/signals.py
  240. 17 19
      misago/threads/tests/test_anonymize_data.py
  241. 0 1
      misago/threads/tests/test_attachmentadmin_views.py
  242. 33 49
      misago/threads/tests/test_attachments_api.py
  243. 10 30
      misago/threads/tests/test_attachments_middleware.py
  244. 0 1
      misago/threads/tests/test_attachmenttypeadmin_views.py
  245. 0 3
      misago/threads/tests/test_clearattachments.py
  246. 1 1
      misago/threads/tests/test_delete_user_likes.py
  247. 0 1
      misago/threads/tests/test_events.py
  248. 2 2
      misago/threads/tests/test_floodprotection.py
  249. 4 23
      misago/threads/tests/test_floodprotection_middleware.py
  250. 0 1
      misago/threads/tests/test_participants.py
  251. 0 8
      misago/threads/tests/test_post_model.py
  252. 54 76
      misago/threads/tests/test_privatethread_patch_api.py
  253. 2 0
      misago/threads/tests/test_privatethread_reply_api.py
  254. 9 14
      misago/threads/tests/test_privatethread_start_api.py
  255. 17 25
      misago/threads/tests/test_privatethreads_api.py
  256. 31 31
      misago/threads/tests/test_search.py
  257. 49 64
      misago/threads/tests/test_thread_bulkpatch_api.py
  258. 28 46
      misago/threads/tests/test_thread_editreply_api.py
  259. 49 167
      misago/threads/tests/test_thread_merge_api.py
  260. 0 11
      misago/threads/tests/test_thread_model.py
  261. 299 206
      misago/threads/tests/test_thread_patch_api.py
  262. 83 117
      misago/threads/tests/test_thread_pollcreate_api.py
  263. 8 31
      misago/threads/tests/test_thread_polldelete_api.py
  264. 148 192
      misago/threads/tests/test_thread_polledit_api.py
  265. 74 314
      misago/threads/tests/test_thread_pollvotes_api.py
  266. 30 79
      misago/threads/tests/test_thread_postbulkdelete_api.py
  267. 25 34
      misago/threads/tests/test_thread_postbulkpatch_api.py
  268. 26 48
      misago/threads/tests/test_thread_postdelete_api.py
  269. 3 6
      misago/threads/tests/test_thread_postedits_api.py
  270. 31 37
      misago/threads/tests/test_thread_postlikes_api.py
  271. 61 90
      misago/threads/tests/test_thread_postmerge_api.py
  272. 74 118
      misago/threads/tests/test_thread_postmove_api.py
  273. 176 145
      misago/threads/tests/test_thread_postpatch_api.py
  274. 7 10
      misago/threads/tests/test_thread_postread_api.py
  275. 40 101
      misago/threads/tests/test_thread_postsplit_api.py
  276. 19 28
      misago/threads/tests/test_thread_reply_api.py
  277. 21 46
      misago/threads/tests/test_thread_start_api.py
  278. 0 1
      misago/threads/tests/test_threadparticipant_model.py
  279. 7 4
      misago/threads/tests/test_threads_api.py
  280. 47 88
      misago/threads/tests/test_threads_bulkdelete_api.py
  281. 34 118
      misago/threads/tests/test_threads_editor_api.py
  282. 108 175
      misago/threads/tests/test_threads_merge_api.py
  283. 232 59
      misago/threads/tests/test_threadslists.py
  284. 18 17
      misago/threads/tests/test_threadview.py
  285. 44 0
      misago/threads/tests/test_updatepostschecksums.py
  286. 1 10
      misago/threads/testutils.py
  287. 0 1
      misago/threads/threadtypes/__init__.py
  288. 2 2
      misago/threads/threadtypes/privatethread.py
  289. 2 2
      misago/threads/threadtypes/thread.py
  290. 1 1
      misago/threads/urls/api.py
  291. 3 2
      misago/threads/validators.py
  292. 2 2
      misago/threads/viewmodels/category.py
  293. 4 3
      misago/threads/viewmodels/thread.py
  294. 2 2
      misago/threads/viewmodels/threads.py
  295. 1 1
      misago/threads/views/list.py
  296. 9 3
      misago/threads/views/thread.py
  297. 1 0
      misago/urls.py
  298. 19 0
      misago/users/admin.py
  299. 126 78
      misago/users/api/auth.py
  300. 24 20
      misago/users/api/userendpoints/avatar.py
  301. 14 11
      misago/users/api/userendpoints/changeemail.py
  302. 16 11
      misago/users/api/userendpoints/changepassword.py
  303. 28 20
      misago/users/api/userendpoints/create.py
  304. 17 14
      misago/users/api/userendpoints/signature.py
  305. 58 51
      misago/users/api/userendpoints/username.py
  306. 40 11
      misago/users/api/users.py
  307. 8 0
      misago/users/apps.py
  308. 19 0
      misago/users/audittrail.py
  309. 0 73
      misago/users/authmixin.py
  310. 4 4
      misago/users/avatars/gallery.py
  311. 5 5
      misago/users/bans.py
  312. 20 9
      misago/users/context_processors.py
  313. 53 0
      misago/users/datadownloads/__init__.py
  314. 156 0
      misago/users/datadownloads/dataarchive.py
  315. 0 0
      misago/users/forms/__init__.py
  316. 94 9
      misago/users/forms/admin.py
  317. 15 5
      misago/users/forms/register.py
  318. 2 2
      misago/users/management/commands/createsuperuser.py
  319. 41 0
      misago/users/management/commands/deleteinactiveusers.py
  320. 24 0
      misago/users/management/commands/expireuserdatadownloads.py
  321. 2 2
      misago/users/management/commands/invalidatebans.py
  322. 2 6
      misago/users/management/commands/populateonlinetracker.py
  323. 42 0
      misago/users/management/commands/prepareuserdatadownloads.py
  324. 18 0
      misago/users/management/commands/removeoldips.py
  325. 3 3
      misago/users/management/commands/synchronizeusers.py
  326. 17 3
      misago/users/middleware.py
  327. 33 0
      misago/users/migrations/0012_audittrail.py
  328. 28 0
      misago/users/migrations/0013_auto_20180609_1523.py
  329. 35 0
      misago/users/migrations/0014_datadownload.py
  330. 21 0
      misago/users/migrations/0015_user_agreements.py
  331. 2 0
      misago/users/models/__init__.py
  332. 18 0
      misago/users/models/audittrail.py
  333. 0 3
      misago/users/models/avatargallery.py
  334. 53 0
      misago/users/models/datadownload.py
  335. 21 20
      misago/users/models/user.py
  336. 27 24
      misago/users/namechanges.py
  337. 16 50
      misago/users/online/tracker.py
  338. 2 13
      misago/users/profilefields/default.py
  339. 32 14
      misago/users/registration.py
  340. 8 14
      misago/users/serializers/__init__.py
  341. 45 97
      misago/users/serializers/auth.py
  342. 9 3
      misago/users/serializers/ban.py
  343. 18 0
      misago/users/serializers/datadownload.py
  344. 5 0
      misago/users/serializers/moderation.py
  345. 21 5
      misago/users/serializers/options.py
  346. 8 0
      misago/users/serializers/rank.py
  347. 0 118
      misago/users/serializers/register.py
  348. 94 1
      misago/users/serializers/user.py
  349. 3 1
      misago/users/serializers/usernamechange.py
  350. 87 1
      misago/users/signals.py
  351. 19 9
      misago/users/social/pipeline.py
  352. 187 0
      misago/users/tests/test_audittrail.py
  353. 94 247
      misago/users/tests/test_auth_api.py
  354. 4 4
      misago/users/tests/test_auth_views.py
  355. 0 10
      misago/users/tests/test_bio_profilefield.py
  356. 324 0
      misago/users/tests/test_datadownloads.py
  357. 245 0
      misago/users/tests/test_datadownloads_dataarchive.py
  358. 133 0
      misago/users/tests/test_datadownloadsadmin_views.py
  359. 109 0
      misago/users/tests/test_deleteinactiveusers.py
  360. 107 0
      misago/users/tests/test_expireuserdatadownloads.py
  361. 0 20
      misago/users/tests/test_gender_profilefield.py
  362. 53 18
      misago/users/tests/test_joinip_profilefield.py
  363. 7 11
      misago/users/tests/test_namechanges.py
  364. 4 7
      misago/users/tests/test_options_views.py
  365. 84 0
      misago/users/tests/test_prepareuserdatadownloads.py
  366. 54 0
      misago/users/tests/test_removeoldips.py
  367. 24 24
      misago/users/tests/test_search.py
  368. 121 1
      misago/users/tests/test_social_pipeline.py
  369. 10 10
      misago/users/tests/test_testutils.py
  370. 0 10
      misago/users/tests/test_twitter_profilefield.py
  371. 27 74
      misago/users/tests/test_user_avatar_api.py
  372. 10 15
      misago/users/tests/test_user_changeemail_api.py
  373. 2 2
      misago/users/tests/test_user_changepassword_api.py
  374. 129 73
      misago/users/tests/test_user_create_api.py
  375. 47 0
      misago/users/tests/test_user_datadownloads_api.py
  376. 3 3
      misago/users/tests/test_user_model.py
  377. 60 0
      misago/users/tests/test_user_requestdatadownload_api.py
  378. 13 29
      misago/users/tests/test_user_signature_api.py
  379. 51 92
      misago/users/tests/test_user_username_api.py
  380. 144 30
      misago/users/tests/test_useradmin_views.py
  381. 7 95
      misago/users/tests/test_usernamechanges_api.py
  382. 23 54
      misago/users/tests/test_users_api.py
  383. 11 2
      misago/users/testutils.py
  384. 2 0
      misago/users/urls/__init__.py
  385. 7 3
      misago/users/urls/api.py
  386. 4 0
      misago/users/views/activation.py
  387. 1 1
      misago/users/views/admin/bans.py
  388. 68 0
      misago/users/views/admin/datadownloads.py
  389. 1 1
      misago/users/views/admin/ranks.py
  390. 24 9
      misago/users/views/admin/users.py
  391. 10 6
      misago/users/views/forgottenpassword.py
  392. 2 2
      misago/users/views/profile.py
  393. 1 0
      requirements.txt
  394. 3 0
      runtests.py

+ 1 - 1
Dockerfile

@@ -1,7 +1,7 @@
 # This dockerfile is only meant for local development of Misago
 # If you are looking for a proper docker setup for running Misago in production,
 # please use misago-docker instead
-FROM python:3
+FROM python:3.5
 
 ENV PYTHONUNBUFFERED 1
 ENV PATH "$PATH:/srv/misago"

+ 2 - 2
MANIFEST.in

@@ -2,8 +2,8 @@ include LICENSE.rst
 include MANIFEST.in
 include README.rst
 include requirements.txt
-graft docs
-prune docs/build
+prune devproject
+prune docs
 prune extras
 prune testproject
 graft misago

+ 6 - 16
README.rst

@@ -10,16 +10,16 @@ Misago
    :target: https://coveralls.io/github/rafalp/Misago?branch=master
    :alt: Test Coverage
 
+.. image:: https://badges.gitter.im/Misago/Misago.svg
+   :target: https://gitter.im/Misago/Misago?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
+   :alt: Development Chat
+
 .. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
    :target: https://travis-ci.org/rafalp/Misago
    :alt: Works on Python 2.7, 3.5, 3,6
 
-.. image:: https://img.shields.io/badge/chat-on_discord-7289da.svg
-   :target: https://discord.gg/fwvrZgB
-   :alt: Community Chat
-
 
-**Development Status:** Big changes happening on master branch, use `latest release <https://github.com/rafalp/Misago/releases>`_ for working code.
+**Development Status:** 🍌 `Bananas <https://en.wikipedia.org/wiki/Perpetual_beta>`_ 🍌
 
 Misago aims to be complete, featured and modern forum solution that has no fear to say 'NO' to common and outdated opinions about how forum software should be made and what it should do.
 
@@ -79,8 +79,6 @@ Even more features will follow in future releases:
 * Ranking system for forum search results based on post links, likes, author and thread importance.
 * Post reactions in place of likes.
 
-...and more!
-
 If you are looking into using Misago to run live forum, you are absolutely invited to, but please keep in mind that Misago is relatively immature software that may contain serious bugs or issues as well as quirks and lackings thay may take time to resolve, despite best efforts. 
 
 
@@ -116,14 +114,6 @@ If you'll ever want to destroy Docker setup because you no longer need it, run t
 Frontend
 --------
 
-.. image:: https://img.shields.io/badge/github-Misago--Frontend-blue.svg
-   :target: https://github.com/rafalp/Misago-Frontend
-   :alt: Tests Result
-
-.. image:: https://travis-ci.org/rafalp/Misago-Frontend.svg?branch=master
-   :target: https://travis-ci.org/rafalp/Misago-Frontend
-   :alt: Tests Result
-
 With exception of Admin Panel, Misago frontend relies heavily on React.js components backed by Django API. This application relies on custom Gulp.js-based toolkit for development. As of current, Misago's ``gulpfile.js`` defines following tasks:
 
 * **build** does production build of Misago's assets, concating and minifying javascripts, css and images, as well as moving them to misago/static directory
@@ -170,7 +160,7 @@ English sentences used within ``misago.faker.phrases`` were extracted from `Nati
 Copyright and license
 =====================
 
-**Misago** - Copyright © 2018 `Rafał Pitoń <http://github.com/ralfp>`_
+**Misago** - Copyright © 2016 `Rafał Pitoń <http://github.com/ralfp>`_
 This program comes with ABSOLUTELY NO WARRANTY.
 
 This is free software and you are welcome to modify and redistribute it under the conditions described in the license.

+ 75 - 13
frontend/src/components/RegisterLegalFootnote.js

@@ -1,22 +1,84 @@
 /* jshint ignore:start */
 import React from 'react';
 import misago from 'misago';
+import escapeHtml from 'misago/utils/escape-html';
 
-const RegisterLegalFootnote = () => {
-  if (!misago.get('TERMS_OF_SERVICE_URL')) return null;
+const AGREEMENT_URL = '<a href="%(url)s" target="_blank">%(agreement)s</a>'
+
+const RegisterLegalFootnote = (props) => {
+  const {
+    errors,
+    privacyPolicy,
+    termsOfService,
+    onPrivacyPolicyChange,
+    onTermsOfServiceChange
+  } = props;
+  
+  const termsOfServiceId = misago.get('TERMS_OF_SERVICE_ID');
+  const termsOfServiceUrl = misago.get('TERMS_OF_SERVICE_URL');
+
+  const privacyPolicyId = misago.get('PRIVACY_POLICY_ID');
+  const privacyPolicyUrl = misago.get('PRIVACY_POLICY_URL');
+
+  if (!termsOfServiceId && !privacyPolicyId) return null;
+
+  return (
+    <div>
+      <LegalAgreement
+        agreement={gettext("the terms of service")}
+        checked={termsOfService !== null}
+        errors={errors.termsOfService}
+        url={termsOfServiceUrl}
+        value={termsOfServiceId}
+        onChange={onTermsOfServiceChange}
+      />
+      <LegalAgreement
+        agreement={gettext("the privacy policy")}
+        checked={privacyPolicy !== null}
+        errors={errors.privacyPolicy}
+        url={privacyPolicyUrl}
+        value={privacyPolicyId}
+        onChange={onPrivacyPolicyChange}
+      />
+    </div>
+  );
+}
+
+const LegalAgreement = (props) => {
+  const { agreement, checked, errors, url, value, onChange } = props;
+
+  if (!url) return;
+
+  const agreementHtml = interpolate(
+    AGREEMENT_URL,
+    { agreement: escapeHtml(agreement), url: escapeHtml(url) },
+    true
+  )
+  const label = interpolate(
+      gettext("I have read and accept %(agreement)s."),
+      { agreement: agreementHtml },
+      true
+  );
 
   return (
-    <p className="legal-footnote">
-      <span className="material-icon">
-        info_outline
-        </span>
-      <a
-        href={misago.get('TERMS_OF_SERVICE_URL')}
-        target="_blank"
-      >
-        {gettext("By registering you agree to site's terms and conditions.")}
-      </a>
-    </p>
+    <div className="checkbox legal-footnote">
+      <label>
+        <input
+          checked={checked}
+          type="checkbox"
+          value={value}
+          onChange={onChange}
+        />
+        <span
+          dangerouslySetInnerHTML={{ __html: label}}
+        />
+      </label>
+      {errors && errors.map((error, i) => (
+        <div className="help-block errors" key={i}>
+          {error}
+        </div>
+      ))}
+    </div>
   );
 }
 

+ 57 - 0
frontend/src/components/accept-agreement.js

@@ -0,0 +1,57 @@
+/* jshint ignore:start */
+import React from 'react';
+import ajax from 'misago/services/ajax'
+
+export default class AcceptAgreement extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = { submiting: false };
+  }
+
+  handleDecline = () => {
+    if (this.state.submiting) return;
+
+    const confirmation = confirm(gettext("Declining will result in immediate deactivation and deletion of your account. This action is not reversible."));
+    if (!confirmation) return;
+
+    this.setState({ submiting: true });
+
+    ajax.post(this.props.api, { accept: false }).then(() => {
+      location.reload(true)
+    })
+  }
+
+  handleAccept = () => {
+    if (this.state.submiting) return;
+
+    this.setState({ submiting: true });
+
+    ajax.post(this.props.api, { accept: true }).then(() => {
+      location.reload(true)
+    })
+  }
+
+  render() {
+    return (
+      <div>
+        <button
+          className="btn btn-default"
+          disabled={this.state.submiting}
+          type="buton"
+          onClick={this.handleDecline}
+        >
+          {gettext("Decline")}
+        </button>
+        <button
+          className="btn btn-primary"
+          disabled={this.state.submiting}
+          type="buton"
+          onClick={this.handleAccept}
+        >
+          {gettext("Accept and continue")}
+        </button>
+      </div>
+    );
+  }
+}

+ 1 - 1
frontend/src/components/avatar.js

@@ -19,7 +19,7 @@ export default function(props) {
 }
 
 export function getSrc(user, size) {
-  if (!!user && user.id) {
+  if (user && user.id) {
     // just avatar hash, size and user id
     return resolveAvatarForSize(user.avatars, size).url;
   } else {

+ 1 - 1
frontend/src/components/options/delete-account.js

@@ -1,6 +1,6 @@
 /* jshint ignore:start */
 import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
+import Button from 'misago/components/button';
 import ajax from 'misago/services/ajax';
 import title from 'misago/services/page-title';
 import snackbar from 'misago/services/snackbar';

+ 156 - 0
frontend/src/components/options/download-data.js

@@ -0,0 +1,156 @@
+/* jshint ignore:start */
+import React from 'react';
+import moment from 'moment';
+import Button from 'misago/components/button';
+import ajax from 'misago/services/ajax';
+import title from 'misago/services/page-title';
+import snackbar from 'misago/services/snackbar';
+
+export default class DownloadData extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      isLoading: false,
+      isSubmiting: false,
+      downloads: []
+    }
+  }
+
+  componentDidMount() {
+    title.set({
+      title: gettext("Download your data"),
+      parent: gettext("Change your options")
+    });
+
+    this.handleLoadDownloads();
+  }
+
+  handleLoadDownloads = () => {
+    ajax.get(this.props.user.api.data_downloads).then(
+      (data) => {
+        this.setState({
+          isLoading: false,
+          downloads: data
+        });
+      },
+      (rejection) => {
+        snackbar.apiError(rejection);
+      }
+    );
+  };
+
+  handleRequestDataDownload = () => {
+    this.setState({ isSubmiting: true });
+    ajax.post(this.props.user.api.request_data_download).then(
+      () => {
+        this.handleLoadDownloads();
+        snackbar.success(gettext("Your request for data download has been registered."));
+        this.setState({ isSubmiting: false });
+      },
+      (rejection) => {
+        console.log(rejection)
+        snackbar.apiError(rejection);
+        this.setState({ isSubmiting: false });
+      }
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <div className="panel panel-default panel-form">
+          <div className="panel-heading">
+            <h3 className="panel-title">{gettext("Download your data")}</h3>
+          </div>
+          <div className="panel-body">
+
+            <p>{gettext("To download your data from the site, click the \"Request data download\" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.")}</p>
+
+            <p>{gettext("The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired.")}</p>
+
+          </div>
+          <table className="table">
+            <thead>
+              <tr>
+                <th>{gettext("Requested on")}</th>
+                <th className="col-md-4">{gettext("Download")}</th>
+              </tr>
+            </thead>
+            <tbody>
+              {this.state.downloads.map((item) => {
+                return (
+                  <tr key={item.id}>
+                    <td style={rowStyle}>{moment(item.requested_on).fromNow()}</td>
+                    <td>
+                      <DownloadButton
+                        exportFile={item.file}
+                        status={item.status}
+                      />
+                    </td>
+                  </tr>
+                )
+              })}
+              {this.state.downloads.length == 0 ?
+                <tr>
+                  <td colSpan="2">{gettext("You have no data downloads.")}</td>
+                </tr> : null}
+            </tbody>
+          </table>
+          <div className="panel-footer text-right">
+            <Button
+              className="btn-primary"
+              loading={this.state.isSubmiting}
+              type="button"
+              onClick={this.handleRequestDataDownload}
+            >
+              {gettext("Request data download")}
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+const rowStyle = {
+  verticalAlign: 'middle'
+};
+
+const STATUS_PENDING = 0;
+const STATUS_PROCESSING = 1;
+
+const DownloadButton = ({ exportFile, status }) => {
+  if (status === STATUS_PENDING || status === STATUS_PROCESSING) {
+    return (
+      <Button
+        className="btn-info btn-sm btn-block"
+        disabled={true}
+        type="button"
+      >
+        {gettext("Download is being prepared")}
+      </Button>
+    );
+  }
+
+  if (exportFile) {
+    return (
+      <a
+        className="btn btn-success btn-sm btn-block"
+        href={exportFile}
+      >
+        {gettext("Download your data")}
+      </a>
+    );
+  }
+
+  return (
+    <Button
+      className="btn-default btn-sm btn-block"
+      disabled={true}
+      type="button"
+    >
+      {gettext("Download is expired")}
+    </Button>
+  );
+}

+ 8 - 0
frontend/src/components/options/root.js

@@ -4,6 +4,7 @@ import DropdownToggle from 'misago/components/dropdown-toggle'; // jshint ignore
 import { SideNav, CompactNav } from 'misago/components/options/navs'; // jshint ignore:line
 import DeleteAccount from 'misago/components/options/delete-account';
 import EditDetails from 'misago/components/options/edit-details';
+import DownloadData from 'misago/components/options/download-data';
 import ChangeForumOptions from 'misago/components/options/forum-options';
 import ChangeUsername from 'misago/components/options/change-username/root';
 import ChangeSignInCredentials from 'misago/components/options/sign-in-credentials/root';
@@ -86,6 +87,13 @@ export function paths() {
     }
   ];
 
+  if (misago.get('ENABLE_DOWNLOAD_OWN_DATA')) {
+    paths.push({
+      path: misago.get('USERCP_URL') + 'download-data/',
+      component: connect(select)(DownloadData)
+    });
+  }
+
   if (misago.get('ENABLE_DELETE_OWN_ACCOUNT')) {
     paths.push({
       path: misago.get('USERCP_URL') + 'delete-account/',

+ 0 - 15
frontend/src/components/posts-list/event/info.js

@@ -13,7 +13,6 @@ export default function(props) {
     <ul className="list-inline event-info">
       <Hidden {...props} />
       <Poster {...props} />
-      <Ip {...props} />
       <Controls {...props} />
     </ul>
   );
@@ -81,18 +80,4 @@ export function Poster(props) {
   return (
     <li className="event-posters" dangerouslySetInnerHTML={{__html: message}} />
   );
-}
-
-export function Ip(props) {
-  if (props.user.acl.can_see_users_ips) {
-    return (
-      <li className="event-ip">
-        <abbr title={props.post.poster_ip}>
-          {gettext("IP recorded")}
-        </abbr>
-      </li>
-    );
-  } else {
-    return null;
-  }
 }

+ 61 - 16
frontend/src/components/register.js

@@ -27,6 +27,29 @@ export class RegisterForm extends Form {
       }
     });
 
+    const formValidators = {
+      username: [
+        validators.usernameContent(),
+          validators.usernameMinLength(username.min_length),
+          validators.usernameMaxLength(username.max_length)
+        ],
+      email: [
+        validators.email()
+      ],
+      password: [
+        validators.passwordMinLength(passwordMinLength)
+      ],
+      captcha: captcha.validator()
+    };
+
+    if (!!misago.get('TERMS_OF_SERVICE_ID')) {
+      formValidators.termsOfService = [validators.requiredTermsOfService()];
+    }
+
+    if (!!misago.get('PRIVACY_POLICY_ID')) {
+      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()];
+    }
+
     this.state = {
       isLoading: false,
 
@@ -35,21 +58,10 @@ export class RegisterForm extends Form {
       password: '',
       captcha: '',
 
-      validators: {
-        username: [
-          validators.usernameContent(),
-          validators.usernameMinLength(username.min_length),
-          validators.usernameMaxLength(username.max_length)
-        ],
-        email: [
-          validators.email()
-        ],
-        password: [
-          validators.passwordMinLength(passwordMinLength)
-        ],
-        captcha: captcha.validator()
-      },
+      termsOfService: null,
+      privacyPolicy: null,
 
+      validators: formValidators,
       errors: {}
     };
   }
@@ -71,7 +83,9 @@ export class RegisterForm extends Form {
       username: this.state.username,
       email: this.state.email,
       password: this.state.password,
-      captcha: this.state.captcha
+      captcha: this.state.captcha,
+      terms_of_service: this.state.termsOfService,
+      privacy_policy: this.state.privacyPolicy
     });
   }
 
@@ -98,6 +112,31 @@ export class RegisterForm extends Form {
     }
   }
 
+  /* jshint ignore:start */
+  handlePrivacyPolicyChange = (event) => {
+    const value = event.target.value;
+    this.handleToggleAgreement('privacyPolicy', value);
+  };
+
+  handleTermsOfServiceChange = (event) => {
+    const value = event.target.value;
+    this.handleToggleAgreement('termsOfService', value);
+  };
+
+  handleToggleAgreement = (agreement, value) => {
+    this.setState((prevState, props) => {
+      if (prevState[agreement] === null) {
+        const errors = { ...prevState.errors, [agreement]: null };
+        return { errors, [agreement]: value };
+      }
+
+      const validator = this.state.validators[agreement][0];
+      const errors = { ...prevState.errors, [agreement]: [validator(null)] };
+      return { errors, [agreement]: null };
+    })
+  };
+  /* jshint ignore:end */
+
   render() {
     /* jshint ignore:start */
     return <div className="modal-dialog modal-register" role="document">
@@ -160,7 +199,13 @@ export class RegisterForm extends Form {
               form: this,
             })}
 
-            <RegisterLegalFootnote />
+            <RegisterLegalFootnote
+              errors={this.state.errors}
+              privacyPolicy={this.state.privacyPolicy}
+              termsOfService={this.state.termsOfService}
+              onPrivacyPolicyChange={this.handlePrivacyPolicyChange}
+              onTermsOfServiceChange={this.handleTermsOfServiceChange}
+            />
 
           </div>
           <div className="modal-footer">

+ 1 - 1
frontend/src/components/social-auth/complete.js

@@ -11,7 +11,7 @@ const Complete = ({ activation, backend_name, username }) => {
   } else if (activation === 'admin') {
     message = gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in.");
   } else {
-    message = gettext("%(username)s, your account has been created and you has been signed in to it.")
+    message = gettext("%(username)s, your account has been created and you have been signed in to it.")
   }
 
   if (activation === 'active') {

+ 71 - 12
frontend/src/components/social-auth/register.js

@@ -1,5 +1,6 @@
 /* jshint ignore:start */
 import React from 'react';
+import misago from 'misago';
 import RegisterLegalFootnote from 'misago/components/RegisterLegalFootnote';
 import Button from 'misago/components/button';
 import Form from 'misago/components/form';
@@ -13,21 +14,33 @@ export default class Register extends Form {
   constructor(props) {
     super(props);
 
+    const formValidators = {
+      email: [
+        validators.email()
+      ],
+      username: [
+        validators.usernameContent()
+      ]
+    };
+
+    if (!!misago.get('TERMS_OF_SERVICE_ID')) {
+      formValidators.termsOfService = [validators.requiredTermsOfService()];
+    }
+
+    if (!!misago.get('PRIVACY_POLICY_ID')) {
+      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()];
+    }
+
     this.state = {
       email: props.email || '',
       emailProtected: !!props.email,
       username: props.username || '',
 
-      errors: {},
+      termsOfService: null,
+      privacyPolicy: null,
 
-      validators: {
-        email: [
-          validators.email()
-        ],
-        username: [
-          validators.usernameContent()
-        ]
-      },
+      validators: formValidators,
+      errors: {},
 
       isLoading: false
     };
@@ -37,7 +50,7 @@ export default class Register extends Form {
     let errors = this.validate();
     let lengths = [
       this.state.email.trim().length,
-      this.state.username.trim().length
+      this.state.username.trim().length,
     ];
 
     if (lengths.indexOf(0) !== -1) {
@@ -45,13 +58,30 @@ export default class Register extends Form {
       return false;
     }
 
+    const { validators } = this.state;
+
+    const checkTermsOfService = !!misago.get('TERMS_OF_SERVICE_ID');
+    if (checkTermsOfService && this.state.termsOfService === null) {
+      snackbar.error(validators.termsOfService[0](null));
+      return false;
+    }
+
+    const checkPrivacyPolicy = !!misago.get('PRIVACY_POLICY_ID');
+    if (checkPrivacyPolicy && this.state.privacyPolicy === null) {
+      snackbar.error(validators.privacyPolicy[0](null));
+      snackbar.error(gettext("You need to accept the privacy policy."));
+      return false;
+    }
+
     return true;
   }
 
   send() {
     return ajax.post(this.props.url, {
       email: this.state.email,
-      username: this.state.username
+      username: this.state.username,
+      terms_of_service: this.state.termsOfService,
+      privacy_policy: this.state.privacyPolicy
     });
   }
 
@@ -76,6 +106,29 @@ export default class Register extends Form {
     }
   }
 
+  handlePrivacyPolicyChange = (event) => {
+    const value = event.target.value;
+    this.handleToggleAgreement('privacyPolicy', value);
+  };
+
+  handleTermsOfServiceChange = (event) => {
+    const value = event.target.value;
+    this.handleToggleAgreement('termsOfService', value);
+  };
+
+  handleToggleAgreement = (agreement, value) => {
+    this.setState((prevState, props) => {
+      if (prevState[agreement] === null) {
+        const errors = { ...prevState.errors, [agreement]: null };
+        return { errors, [agreement]: value };
+      }
+
+      const validator = this.state.validators[agreement][0];
+      const errors = { ...prevState.errors, [agreement]: [validator(null)] };
+      return { errors, [agreement]: null };
+    })
+  };
+
   render() {
     const { backend_name } = this.props;
     const {
@@ -132,7 +185,13 @@ export default class Register extends Form {
                         value={email}
                       />
                     </FormGroup>
-                    <RegisterLegalFootnote />
+                    <RegisterLegalFootnote
+                      errors={this.state.errors}
+                      privacyPolicy={this.state.privacyPolicy}
+                      termsOfService={this.state.termsOfService}
+                      onPrivacyPolicyChange={this.handlePrivacyPolicyChange}
+                      onTermsOfServiceChange={this.handleTermsOfServiceChange}
+                    />
                   </div>
                   <div className="panel-footer">
                     <Button className="btn-primary" loading={this.state.isLoading}>

+ 21 - 0
frontend/src/initializers/components/accept-agreement.js

@@ -0,0 +1,21 @@
+/* jshint ignore:start */
+import React from 'react';
+import misago from 'misago/index';
+import AcceptAgreement from 'misago/components/accept-agreement';
+import mount from 'misago/utils/mount-component';
+
+export default function initializer(context) {
+  if (document.getElementById('required-agreement-mount')) {
+    mount(
+      <AcceptAgreement api={context.get('REQUIRED_AGREEMENT_API')} />,
+      'required-agreement-mount',
+      false
+    );
+  }
+}
+
+misago.addInitializer({
+  name: 'component:accept-agreement',
+  initializer: initializer,
+  after: 'store'
+});

+ 13 - 3
frontend/src/utils/validators.js

@@ -1,14 +1,24 @@
 const EMAIL = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
 const USERNAME = new RegExp('^[0-9a-z]+$', 'i');
 
-export function required() {
+export function required(message) {
   return function(value) {
-    if ($.trim(value).length === 0) {
-      return gettext("This field is required.");
+    if (value === false || value === null || $.trim(value).length === 0) {
+      return message || gettext("This field is required.");
     }
   };
 }
 
+export function requiredTermsOfService(message) {
+  const error = gettext("You have to accept the terms of service.");
+  return required(message || error);
+}
+
+export function requiredPrivacyPolicy(message) {
+  const error = gettext("You have to accept the privacy policy.");
+  return required(message || error);
+}
+
 export function email(message) {
   return function(value) {
     if (!EMAIL.test(value)) {

+ 5 - 15
frontend/style/flavor/modals.less

@@ -57,24 +57,14 @@
 
 // Register modal
 .legal-footnote {
-  text-align: center;
-
-  .material-icon {
-    margin-right: 4px;
-    position: relative;
-    bottom: 1px;
-
-    color: @gray-light;
-    font-size: 28px;
-    line-height: 28px;
+  label {
+    font-weight: bold;
   }
 
-  a, a:link, a:visited {
-    color: @gray-light;
-  }
+  .help-block {
+    margin-left: 20px;
 
-  a:focus, a:hover, a:active {
-    color: @text-color;
+    color: @brand-danger;
   }
 }
 

+ 1 - 0
frontend/style/index.less

@@ -70,6 +70,7 @@
 // Components
 @import "misago/auth-message.less";
 @import "misago/alerts-snackbar.less";
+@import "misago/agreement-overlay.less";
 @import "misago/loaders.less";
 @import "misago/navbar.less";
 @import "misago/navs.less";

+ 68 - 0
frontend/style/misago/agreement-overlay.less

@@ -0,0 +1,68 @@
+//
+// Agreement overlay
+// --------------------------------------------------
+
+
+.agreement-overlay {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  top: 0px;
+  left: 0px;
+  z-index: @zindex-modal + 10;
+
+  background-color: fadeOut(@modal-backdrop-bg, @modal-backdrop-opacity * 100);
+  overflow-x: scroll;
+  
+  .container {
+    max-width: @modal-lg;
+    padding: @line-height-computed @grid-gutter-width;
+  }
+}
+
+.agreement-content {
+  background-color: @modal-content-bg;
+  border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)
+  border: 1px solid @modal-content-border-color;
+  border-radius: @border-radius-large;
+  .box-shadow(0 3px 9px rgba(0,0,0,.5));
+  background-clip: padding-box;
+  outline: 0;
+
+  padding: @padding-large-vertical @padding-large-horizontal;
+}
+
+.agreement-header {
+  padding: @padding-large-vertical 0;
+
+  border-bottom: 1px solid @gray-lighter;
+
+  h2 {
+    margin-top: 0;
+  }
+
+  p {
+    margin: 0;
+    padding: 0;
+  }
+}
+
+.agreement-body {
+  padding: @line-height-computed 0;
+
+  p.lead {
+    margin: 0;
+    padding: 0;
+  }
+}
+
+.agreement-footer {
+  .modal-footer();
+
+  padding: @padding-large-vertical 0;
+}
+
+// utility class disabling scroll on body
+body.agreement-overlay-visible {
+  overflow: hidden;
+}

+ 1 - 1
initdev

@@ -54,4 +54,4 @@ echo "  docker-compose run --rm misago initdev --f"
 echo ""
 echo -e "${RED}Warning:${DEFAULT} if you have uncommited changes to Misago's setup that should be included"
 echo "in next release, make sure that they are commited to 'misago/project_template'"
-echo "or 'initdev --f' will overwrite the files causing them to be lost."
+echo "or 'initdev --f' will overwrite the files causing them to be lost."

+ 1 - 1
misago/__init__.py

@@ -1 +1 @@
-__version__ = '0.18.1'
+__version__ = '0.19.0'

+ 1 - 2
misago/acl/testutils.py

@@ -27,12 +27,11 @@ def override_acl(user, new_acl):
     """overrides user permissions with specified ones"""
     final_cache = deepcopy(user.acl_cache)
     final_cache.update(new_acl)
-
+    
     if user.is_authenticated:
         user._acl_cache = final_cache
         user.acl_key = md5(str(user.pk).encode()).hexdigest()[:8]
         user.save(update_fields=['acl_key'])
-
         threadstore.set('acl_%s' % user.acl_key, final_cache)
     else:
         threadstore.set('acl_%s' % user.acl_key, final_cache)

+ 0 - 48
misago/admin/forms.py

@@ -1,48 +0,0 @@
-from django import forms
-from django.contrib.auth import authenticate
-from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _
-
-from misago.users.authmixin import AuthMixin
-
-
-class AdminAuthenticationForm(BaseAuthenticationForm, AuthMixin):
-    username = forms.CharField(
-        label=_("Username or e-mail"),
-        required=False,
-        max_length=254,
-    )
-    password = forms.CharField(
-        label=_("Password"),
-        strip=False,
-        required=False,
-        widget=forms.PasswordInput,
-    )
-
-    error_messages = {
-        'empty_data': _("Fill out both fields."),
-        'invalid_login': _("Login or password is incorrect."),
-        'not_staff': _("Your account does not have admin privileges."),
-    }
-    required_css_class = 'required'
-
-    def clean(self):
-        username = self.cleaned_data.get('username')
-        password = self.cleaned_data.get('password')
-
-        if username and password:
-            self.user_cache = authenticate(username=username, password=password)
-
-            if self.user_cache is None or not self.user_cache.is_active:
-                raise ValidationError(self.error_messages['invalid_login'], code='invalid_login')
-            else:
-                self.confirm_login_allowed(self.user_cache)
-        else:
-            raise ValidationError(self.error_messages['empty_data'], code='empty_data')
-
-        return self.cleaned_data
-
-    def confirm_login_allowed(self, user):
-        if not user.is_staff:
-            raise ValidationError(self.error_messages['not_staff'], code='not_staff')

+ 67 - 0
misago/admin/tests/test_admin_index.py

@@ -0,0 +1,67 @@
+from django.test import TestCase, override_settings
+from django.urls import reverse
+
+from misago.admin.testutils import AdminTestCase
+from misago.admin.views.index import check_misago_address
+
+
+class AdminIndexViewTests(AdminTestCase):
+    def test_view_returns_200(self):
+        """admin index view returns 200"""
+        response = self.client.get(reverse('misago:admin:index'))
+
+        self.assertContains(response, self.user.username)
+
+    def test_view_contains_address_check(self):
+        """admin index view contains address check"""
+        response = self.client.get(reverse('misago:admin:index'))
+
+        self.assertContains(response, "MISAGO_ADDRESS")
+
+
+class RequestMock(object):
+    absolute_uri = 'https://misago-project.org/somewhere/'
+
+    def build_absolute_uri(self, location):
+        assert location == '/'
+        return self.absolute_uri
+
+
+request = RequestMock()
+incorrect_address = 'http://somewhere.com'
+correct_address = request.absolute_uri
+
+
+class AdminIndexAddressCheckTests(TestCase):
+    @override_settings(MISAGO_ADDRESS=None)
+    def test_address_not_set(self):
+        """check handles address not set"""
+        result = check_misago_address(request)
+
+        self.assertEqual(result, {
+            'is_correct': False,
+            'set_address': None,
+            'correct_address': request.absolute_uri,
+        })
+
+    @override_settings(MISAGO_ADDRESS=incorrect_address)
+    def test_address_set_invalid(self):
+        """check handles incorrect address"""
+        result = check_misago_address(request)
+
+        self.assertEqual(result, {
+            'is_correct': False,
+            'set_address': incorrect_address,
+            'correct_address': request.absolute_uri,
+        })
+
+    @override_settings(MISAGO_ADDRESS=correct_address)
+    def test_address_set_valid(self):
+        """check handles correct address"""
+        result = check_misago_address(request)
+
+        self.assertEqual(result, {
+            'is_correct': True,
+            'set_address': correct_address,
+            'correct_address': request.absolute_uri,
+        })

+ 0 - 13
misago/admin/tests/test_admin_views.py

@@ -133,11 +133,6 @@ class AdminLoginViewTests(TestCase):
 
         self.assertEqual(response.status_code, 302)
 
-    def test_login_no_data(self):
-        """login passess user thats staff and superuser"""
-        response = self.client.post(reverse('misago:admin:index'))
-        self.assertEqual(response.status_code, 200)
-
 
 class AdminLogoutTests(AdminTestCase):
     def test_admin_logout(self):
@@ -201,14 +196,6 @@ class AdminViewAccessTests(AdminTestCase):
         self.assertContains(response, self.user.username)
 
 
-class AdminIndexViewTests(AdminTestCase):
-    def test_view_returns_200(self):
-        """admin index view returns 200"""
-        response = self.client.get(reverse('misago:admin:index'))
-
-        self.assertContains(response, self.user.username)
-
-
 class Admin404ErrorTests(AdminTestCase):
     def test_list_search_unicode_handling(self):
         """querystring creation handles unicode strings"""

+ 1 - 1
misago/admin/views/auth.py

@@ -6,7 +6,7 @@ from django.views.decorators.csrf import csrf_protect
 from django.views.decorators.debug import sensitive_post_parameters
 
 from misago.admin import auth
-from misago.admin.forms import AdminAuthenticationForm
+from misago.users.forms.auth import AdminAuthenticationForm
 
 
 @sensitive_post_parameters()

+ 13 - 0
misago/admin/views/index.py

@@ -12,6 +12,7 @@ from django.http import Http404, JsonResponse
 from django.utils.translation import ugettext as _
 
 from misago import __version__
+from misago.conf import settings
 from misago.core.cache import cache
 from misago.threads.models import Post, Thread
 
@@ -38,6 +39,7 @@ def admin_index(request):
     return render(
         request, 'misago/admin/index.html', {
             'db_stats': db_stats,
+            'address_check': check_misago_address(request),
 
             'allow_version_check': ALLOW_VERSION_CHECK,
             'version_check': cache.get(VERSION_CHECK_CACHE_KEY),
@@ -45,6 +47,17 @@ def admin_index(request):
     )
 
 
+def check_misago_address(request):
+    set_address = settings.MISAGO_ADDRESS
+    correct_address = request.build_absolute_uri('/')
+
+    return {
+        'is_correct': set_address == correct_address,
+        'set_address': set_address,
+        'correct_address': correct_address,
+    }
+
+
 def check_version(request):
     if not ALLOW_VERSION_CHECK or request.method != "POST":
         raise Http404()

+ 0 - 1
misago/api/__init__.py

@@ -1 +0,0 @@
-default_app_config = 'misago.api.apps.MisagoApiConfig'

+ 0 - 7
misago/api/apps.py

@@ -1,7 +0,0 @@
-from django.apps import AppConfig
-
-
-class MisagoApiConfig(AppConfig):
-    name = 'misago.api'
-    label = 'misago_api'
-    verbose_name = "Misago Api"

+ 0 - 7
misago/api/context_processors.py

@@ -1,7 +0,0 @@
-def frontend_context(request):
-    if request.include_frontend_context:
-        return {
-            'frontend_context': request.frontend_context,
-        }
-    else:
-        return {}

+ 0 - 25
misago/api/exceptionhandler.py

@@ -1,25 +0,0 @@
-from rest_framework.views import exception_handler as drf_exception_handler
-
-from django.core.exceptions import PermissionDenied
-from django.http import Http404
-from django.utils import six
-
-from misago.core.exceptions import Banned
-
-
-def handle_api_exception(exception, context):
-    response = drf_exception_handler(exception, context)
-    if response:
-        if isinstance(exception, Banned):
-            response.data = exception.ban.get_serialized_message()
-        elif isinstance(exception, PermissionDenied) and exception.args:
-            response.data = {
-                'detail': six.text_type(exception),
-            }
-        elif isinstance(exception, Http404):
-            response.data = {
-                'detail': 'NOT FOUND',
-            }
-        return response
-    else:
-        return None

+ 0 - 15
misago/api/middleware.py

@@ -1,15 +0,0 @@
-from django.urls import reverse
-from django.utils.deprecation import MiddlewareMixin
-
-
-class FrontendContextMiddleware(MiddlewareMixin):
-    def process_request(self, request):
-        request.include_frontend_context = True
-        request.frontend_context = {
-            'auth': {},
-            'conf': {},
-            'store': {},
-
-            'api': '{}api/'.format(reverse('misago:index')),
-            'url': {},
-        }

+ 0 - 155
misago/api/patch.py

@@ -1,155 +0,0 @@
-from __future__ import unicode_literals
-
-from rest_framework import serializers
-from rest_framework.response import Response
-from rest_framework.settings import api_settings
-
-from django.core.exceptions import PermissionDenied, ValidationError
-from django.db import transaction
-from django.http import Http404
-from django.utils import six
-from django.utils.translation import gettext as _
-
-
-ALLOWED_OPS = ('add', 'remove', 'replace')
-
-
-class InvalidAction(Exception):
-    pass
-
-
-HANDLED_EXCEPTIONS = (
-    serializers.ValidationError,
-    ValidationError,
-    InvalidAction,
-    PermissionDenied,
-    Http404,
-)
-
-
-class ApiPatch(object):
-    def __init__(self):
-        self._actions = []
-
-    def add(self, path, handler):
-        self._actions.append({
-            'op': 'add',
-            'path': path,
-            'handler': handler,
-        })
-
-    def remove(self, path, handler):
-        self._actions.append({
-            'op': 'remove',
-            'path': path,
-            'handler': handler,
-        })
-
-    def replace(self, path, handler):
-        self._actions.append({
-            'op': 'replace',
-            'path': path,
-            'handler': handler,
-        })
-
-    def dispatch(self, request, target):
-        response = {'id': target.pk}
-
-        try:
-            self.validate_actions(request.data)
-        except HANDLED_EXCEPTIONS as exception:
-            return self.handle_exception(exception)
-
-        for action in request.data:
-            try:
-                self.dispatch_action(response, request, target, action)
-            except HANDLED_EXCEPTIONS as exception:
-                return self.handle_exception(exception)
-
-        return Response(response)
-
-    def dispatch_bulk(self, request, targets):
-        try:
-            self.validate_actions(request.data['ops'])
-        except HANDLED_EXCEPTIONS as exception:
-            return self.handle_exception(exception)
-
-        result = []
-        for target in targets:
-            data = {'id': target.pk, 'status': 200, 'patch': {}}
-            for action in request.data['ops']:
-                try:
-                    self.dispatch_action(data['patch'], request, target, action)
-                except HANDLED_EXCEPTIONS as exception:
-                    data, status = self.get_error_data_status(exception)
-                    data.update({'id': target.pk, 'status': status, })
-                    break
-            result.append(data)
-
-        # sort result items by id then cast id and status to string
-        # so we are compliant with our bulk actions spec
-        result.sort(key=lambda item: item['id'])
-        for data in result:
-            data['id'] = str(data['id'])
-            data['status'] = str(data['status'])
-
-        # always returning 200 on op error saves us logic duplication
-        # in the frontend, were we need to do success handling in both
-        # success and error handles
-        return Response(result)
-
-    def validate_actions(self, actions):
-        if not isinstance(actions, list):
-            raise InvalidAction(_("PATCH request should be a list of operations."))
-
-        reduced_actions = []
-        for action in actions:
-            self.validate_action(action)
-
-            reduced_action = self.reduce_action(action)
-            if reduced_action in reduced_actions:
-                raise InvalidAction(
-                    _('"%(op)s" op for "%(path)s" path is repeated.') % reduced_action)
-            reduced_actions.append(reduced_action)
-
-    def validate_action(self, action):
-        if not action.get('op'):
-            raise InvalidAction(_('"op" parameter must be defined.'))
-
-        if action.get('op') not in ALLOWED_OPS:
-            raise InvalidAction(_('"%s" op is unsupported.') % action.get('op'))
-
-        if not action.get('path'):
-            raise InvalidAction(_('"%s" op has to specify path.') % action.get('op'))
-
-        if 'value' not in action:
-            raise InvalidAction(_('"%s" op has to specify value.') % action.get('op'))
-
-    def reduce_action(self, action):
-        return {'op': action['op'], 'path': action['path']}
-
-    def dispatch_action(self, data, request, target, action):
-        for handler in self._actions:
-            if action['op'] == handler['op'] and action['path'] == handler['path']:
-                with transaction.atomic():
-                    data.update(handler['handler'](request, target, action['value']))
-
-    def handle_exception(self, exception):
-        data, status = self.get_error_data_status(exception)
-        return Response(data, status=status)
-
-    def get_error_data_status(self, exception):
-        if isinstance(exception, InvalidAction):
-            return {api_settings.NON_FIELD_ERRORS_KEY: [six.text_type(exception)]}, 400
-
-        if isinstance(exception, serializers.ValidationError):
-            return {'value': exception.detail}, 400
-
-        if isinstance(exception, ValidationError):
-            return {'value': exception.messages}, 400
-
-        if isinstance(exception, PermissionDenied):
-            return {'detail': six.text_type(exception)}, 403
-
-        if isinstance(exception, Http404):
-            return {'detail': 'NOT FOUND'}, 404

+ 0 - 26
misago/api/tests/test_context_processors.py

@@ -1,26 +0,0 @@
-from django.test import TestCase
-
-from misago.api import context_processors
-
-
-class MockRequest(object):
-    pass
-
-
-class FrontendContextTests(TestCase):
-    def test_frontend_context(self):
-        """frontend_context is available in templates"""
-        mock_request = MockRequest()
-        mock_request.include_frontend_context = True
-        mock_request.frontend_context = {'someValue': 'Something'}
-
-        self.assertEqual(
-            context_processors.frontend_context(mock_request), {
-                'frontend_context': {
-                    'someValue': 'Something',
-                },
-            }
-        )
-
-        mock_request.include_frontend_context = False
-        self.assertEqual(context_processors.frontend_context(mock_request), {})

+ 0 - 60
misago/api/tests/test_exceptionhandler.py

@@ -1,60 +0,0 @@
-from django.core import exceptions as django_exceptions
-from django.core.exceptions import PermissionDenied
-from django.http import Http404
-from django.test import TestCase
-
-from misago.api import exceptionhandler
-from misago.core.exceptions import Banned
-from misago.users.models import Ban
-
-
-INVALID_EXCEPTIONS = [
-    django_exceptions.ObjectDoesNotExist,
-    django_exceptions.ViewDoesNotExist,
-    TypeError,
-    ValueError,
-    KeyError,
-]
-
-
-class HandleAPIExceptionTests(TestCase):
-    def test_banned(self):
-        """banned exception is correctly handled"""
-        ban = Ban(user_message="This is test ban!")
-
-        response = exceptionhandler.handle_api_exception(Banned(ban), None)
-
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.data['detail']['html'], "<p>This is test ban!</p>")
-        self.assertIn('expires_on', response.data)
-
-    def test_permission_denied(self):
-        """permission denied exception is correctly handled"""
-        response = exceptionhandler.handle_api_exception(PermissionDenied(), None)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.data, {'detail': "Permission denied."})
-
-    def test_permission_denied_message(self):
-        """permission denied with message is correctly handled"""
-        exception = PermissionDenied("You shall not pass!")
-        response = exceptionhandler.handle_api_exception(exception, None)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.data, {'detail': "You shall not pass!"})
-
-    def test_not_found(self):
-        """exception handler sets NOT FOUND as default message for 404"""
-        response = exceptionhandler.handle_api_exception(Http404(), None)
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.data, {'detail': 'NOT FOUND'})
-
-    def test_not_found_message(self):
-        """exception handler hides custom message behind 404 error"""
-        response = exceptionhandler.handle_api_exception(Http404("Nada!"), None)
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.data, {'detail': 'NOT FOUND'})
-
-    def test_unhandled_exception(self):
-        """our exception handler is not interrupting other exceptions"""
-        for exception in INVALID_EXCEPTIONS:
-            response = exceptionhandler.handle_api_exception(exception(), None)
-            self.assertIsNone(response)

+ 0 - 171
misago/api/tests/test_patch.py

@@ -1,171 +0,0 @@
-from __future__ import unicode_literals
-
-from django.test import TestCase
-
-from misago.api.patch import ApiPatch, InvalidAction
-
-
-class MockObject(object):
-    def __init__(self, pk):
-        self.id = pk
-        self.pk = pk
-
-
-class ApiPatchTests(TestCase):
-    def test_add(self):
-        """add method adds function to patch object"""
-        patch = ApiPatch()
-
-        def mock_function():
-            pass
-
-        patch.add('test-add', mock_function)
-
-        self.assertEqual(len(patch._actions), 1)
-        self.assertEqual(patch._actions[0]['op'], 'add')
-        self.assertEqual(patch._actions[0]['path'], 'test-add')
-        self.assertEqual(patch._actions[0]['handler'], mock_function)
-
-    def test_remove(self):
-        """remove method adds function to patch object"""
-        patch = ApiPatch()
-
-        def mock_function():
-            pass
-
-        patch.remove('test-remove', mock_function)
-
-        self.assertEqual(len(patch._actions), 1)
-        self.assertEqual(patch._actions[0]['op'], 'remove')
-        self.assertEqual(patch._actions[0]['path'], 'test-remove')
-        self.assertEqual(patch._actions[0]['handler'], mock_function)
-
-    def test_replace(self):
-        """replace method adds function to patch object"""
-        patch = ApiPatch()
-
-        def mock_function():
-            pass
-
-        patch.replace('test-replace', mock_function)
-
-        self.assertEqual(len(patch._actions), 1)
-        self.assertEqual(patch._actions[0]['op'], 'replace')
-        self.assertEqual(patch._actions[0]['path'], 'test-replace')
-        self.assertEqual(patch._actions[0]['handler'], mock_function)
-
-    def test_validate_actions(self):
-        """validate_actions method validates action dict"""
-        patch = ApiPatch()
-
-        VALID_ACTIONS = [
-            {
-                'op': 'add',
-                'path': 'test',
-                'value': 42
-            },
-            {
-                'op': 'remove',
-                'path': 'other-test',
-                'value': 'Lorem'
-            },
-            {
-                'op': 'replace',
-                'path': 'false-test',
-                'value': None
-            },
-        ]
-
-        for action in VALID_ACTIONS:
-            patch.validate_actions([action])
-
-        # undefined op
-        UNSUPPORTED_ACTIONS = ({}, {'op': ''}, {'no': 'op'}, )
-
-        for action in UNSUPPORTED_ACTIONS:
-            try:
-                patch.validate_actions([action])
-            except InvalidAction as e:
-                self.assertEqual(e.args[0], '"op" parameter must be defined.')
-
-        # unsupported op
-        try:
-            patch.validate_actions([{'op': 'nope'}])
-        except InvalidAction as e:
-            self.assertEqual(e.args[0], '"nope" op is unsupported.')
-
-        # op lacking patch
-        try:
-            patch.validate_actions([{'op': 'add'}])
-        except InvalidAction as e:
-            self.assertEqual(e.args[0], '"add" op has to specify path.')
-
-        # op lacking value
-        try:
-            patch.validate_actions([{
-                'op': 'add',
-                'path': 'yolo',
-            }])
-        except InvalidAction as e:
-            self.assertEqual(e.args[0], '"add" op has to specify value.')
-
-        # empty value is forbidden
-        try:
-            patch.validate_actions([{
-                'op': 'add',
-                'path': 'yolo',
-                'value': '',
-            }])
-        except InvalidAction as e:
-            self.assertEqual(e.args[0], '"add" op has to specify value.')
-
-        # duplicated actions are forbidden
-        try:
-            patch.validate_actions([
-                {'op': 'add', 'path': 'like', 'value': True},
-                {'op': 'add', 'path': 'like', 'value': False},
-            ])
-        except InvalidAction as e:
-            self.assertEqual(e.args[0], '"add" op for "like" path is repeated.')
-
-    def test_dispatch_action(self):
-        """dispatch_action calls specified actions"""
-        patch = ApiPatch()
-
-        mock_target = MockObject(13)
-
-        def action_a(request, target, value):
-            self.assertEqual(request, 'request')
-            self.assertEqual(target, mock_target)
-            return {'a': value * 2, 'b': 111}
-
-        patch.replace('abc', action_a)
-
-        def action_b(request, target, value):
-            self.assertEqual(request, 'request')
-            self.assertEqual(target, mock_target)
-            return {'b': value * 10}
-
-        patch.replace('abc', action_b)
-
-        def action_fail(request, target, value):
-            self.fail("unrequired action was called")
-
-        patch.add('c', action_fail)
-        patch.remove('c', action_fail)
-        patch.replace('c', action_fail)
-
-        patch_dict = {'id': 123}
-
-        patch.dispatch_action(
-            patch_dict, 'request', mock_target, {
-                'op': 'replace',
-                'path': 'abc',
-                'value': 5,
-            }
-        )
-
-        self.assertEqual(len(patch_dict), 3)
-        self.assertEqual(patch_dict['id'], 123)
-        self.assertEqual(patch_dict['a'], 10)
-        self.assertEqual(patch_dict['b'], 50)

+ 0 - 201
misago/api/tests/test_patch_dispatch.py

@@ -1,201 +0,0 @@
-from __future__ import unicode_literals
-
-from rest_framework.exceptions import ValidationError as ApiValidationError
-
-from django.core.exceptions import PermissionDenied, ValidationError
-from django.http import Http404
-from django.test import TestCase
-
-from misago.api.patch import ApiPatch
-
-
-class MockRequest(object):
-    def __init__(self, data=None):
-        self.data = data
-
-
-class MockObject(object):
-    def __init__(self, pk):
-        self.id = pk
-        self.pk = pk
-
-
-class ApiPatchDispatchTests(TestCase):
-    def test_dispatch(self):
-        """dispatch calls actions and returns response"""
-        patch = ApiPatch()
-
-        def action_error(request, target, value):
-            if value == '404':
-                raise Http404()
-            if value == '404_reason':
-                raise Http404("something was removed")
-            if value == 'perm':
-                raise PermissionDenied("yo ain't doing that!")
-            if value == 'invalid':
-                raise ValidationError("invalid data here!")
-            if value == 'api_invalid':
-                raise ApiValidationError("invalid api data here!")
-
-        patch.replace('error', action_error)
-
-        def action_mutate(request, target, value):
-            return {'value': value * 2}
-
-        patch.replace('mutate', action_mutate)
-
-        # dispatch requires list as an argument
-        response = patch.dispatch(MockRequest({}), MockObject(13))
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ["PATCH request should be a list of operations."],
-        })
-
-        # valid dispatch
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, {'value': 12, 'id': 13})
-
-        # invalid action in dispatch
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ['"replace" op has to specify path.'],
-        })
-
-        # repeated action in dispatch
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 12,
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ['"replace" op for "mutate" path is repeated.'],
-        })
-
-        # op raised validation error
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'invalid',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {'value': ["invalid data here!"]})
-
-        # op raised api validation error
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'api_invalid',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {'value': ["invalid api data here!"]})
-
-        # action in dispatch raised perm denied
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'perm',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.data, {'detail': "yo ain't doing that!"})
-
-        # action in dispatch raised 404
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': '404',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.data, {'detail': 'NOT FOUND'})
-
-        # action in dispatch raised 404 with message but didn't expose it
-        response = patch.dispatch(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 2,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': '404_reason',
-                },
-            ]), MockObject(13)
-        )
-
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.data, {'detail': 'NOT FOUND'})

+ 0 - 227
misago/api/tests/test_patch_dispatch_bulk.py

@@ -1,227 +0,0 @@
-from __future__ import unicode_literals
-
-from rest_framework.exceptions import ValidationError as ApiValidationError
-
-from django.core.exceptions import PermissionDenied, ValidationError
-from django.http import Http404
-from django.test import TestCase
-
-from misago.api.patch import ApiPatch, InvalidAction
-
-
-class MockRequest(object):
-    def __init__(self, ops):
-        self.data = {'ops': ops}
-
-
-class MockObject(object):
-    def __init__(self, pk):
-        self.id = pk
-        self.pk = pk
-
-
-class ApiPatchDispatchBulkTests(TestCase):
-    def test_dispatch_bulk(self):
-        """dispatch_bulk calls actions and returns response"""
-        patch = ApiPatch()
-
-        def action_error(request, target, value):
-            if value == '404':
-                raise Http404()
-            if value == '404_reason':
-                raise Http404("something was removed")
-            if value == 'perm':
-                raise PermissionDenied("yo ain't doing that!")
-            if value == 'invalid':
-                raise ValidationError("invalid data here!")
-            if value == 'api_invalid':
-                raise ApiValidationError("invalid api data here!")
-
-        patch.replace('error', action_error)
-
-        def action_mutate(request, target, value):
-            return {'value': value * 2}
-
-        patch.replace('mutate', action_mutate)
-
-        # valid bulk dispatch
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '200', 'patch': {'value': 12}},
-            {'id': '7', 'status': '200', 'patch': {'value': 12}},
-        ])
-
-        # dispatch requires list as an argument
-        response = patch.dispatch_bulk(MockRequest({}), [MockObject(5), MockObject(7)])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ["PATCH request should be a list of operations."],
-        })
-
-        # invalid action in bulk dispatch
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ['"replace" op has to specify path.'],
-        })
-
-        # repeated action in dispatch
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 12,
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.data, {
-            'non_field_errors': ['"replace" op for "mutate" path is repeated.'],
-        })
-
-        # op raised validation error
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'invalid',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '400', 'value': ["invalid data here!"]},
-            {'id': '7', 'status': '400', 'value': ["invalid data here!"]},
-        ])
-
-        # op raised api validation error
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'api_invalid',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '400', 'value': ["invalid api data here!"]},
-            {'id': '7', 'status': '400', 'value': ["invalid api data here!"]},
-        ])
-
-        # action in bulk dispatch raised perm denied
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': 'perm',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '403', 'detail': "yo ain't doing that!"},
-            {'id': '7', 'status': '403', 'detail': "yo ain't doing that!"},
-        ])
-
-        # action in bulk dispatch raised 404
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 6,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': '404',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '404', 'detail': 'NOT FOUND'},
-            {'id': '7', 'status': '404', 'detail': 'NOT FOUND'},
-        ])
-
-        # action in dispatch raised 404 with message but didn't expose it
-        response = patch.dispatch_bulk(
-            MockRequest([
-                {
-                    'op': 'replace',
-                    'path': 'mutate',
-                    'value': 2,
-                },
-                {
-                    'op': 'replace',
-                    'path': 'error',
-                    'value': '404_reason',
-                },
-            ]),
-            [MockObject(5), MockObject(7)],
-        )
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data, [
-            {'id': '5', 'status': '404', 'detail': 'NOT FOUND'},
-            {'id': '7', 'status': '404', 'detail': 'NOT FOUND'},
-        ])

+ 0 - 20
misago/api/testutils.py

@@ -1,20 +0,0 @@
-class ApiTestsMixin(object):
-    def assertApiResultsAreEmpty(self, response):
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json()['results'], [])
-
-    def assertApiResultsEqual(self, response, items):
-        self.assertEqual(response.status_code, 200)
-        results_ids = [r['id'] for r in response.json()['results']]
-        items_ids = [i.id for i in items]
-        self.assertEqual(results_ids, items_ids)
-
-    def assertInApiResults(self, response, item):
-        self.assertEqual(response.status_code, 200)
-        results_ids = [r['id'] for r in response.json()['results']]
-        self.assertIn(item.id, results_ids)
-
-    def assertNotInApiResults(self, response, item):
-        self.assertEqual(response.status_code, 200)
-        results_ids = [r['id'] for r in response.json()['results']]
-        self.assertNotIn(item.id, results_ids)

+ 2 - 0
misago/categories/__init__.py

@@ -1 +1,3 @@
+from .constants import *
+
 default_app_config = 'misago.categories.apps.MisagoCategoriesConfig'

+ 2 - 0
misago/categories/constants.py

@@ -0,0 +1,2 @@
+PRIVATE_THREADS_ROOT_NAME = 'private_threads'
+THREADS_ROOT_NAME = 'root_category'

+ 0 - 27
misago/categories/context_processors.py

@@ -1,27 +0,0 @@
-from django.db.models import Q
-
-from misago.core.cache import cache
-
-from .serializers import BasicCategorySerializer
-from .models import Category
-
-
-def preload_categories_json(request):
-    try:
-        user_acl_key = request.user.acl_key
-    except AttributeError:
-        return {}
-        
-    cache_key = 'misago_categories_json_{}'.format(user_acl_key)
-    categories_json = cache.get(cache_key, 'nada')
-    if categories_json == 'nada':
-        is_root = Q(level=0)
-        is_visible = Q(id__in=request.user.acl_cache['visible_categories'])
-
-        queryset = Category.objects.all_categories(include_root=True)
-        queryset = queryset.filter(is_root | is_visible)
-
-        categories_json = BasicCategorySerializer(queryset, many=True).data
-        cache.set(cache_key, categories_json, 15 * 60)
-    request.frontend_context['store']['categories'] = categories_json
-    return {}

+ 3 - 2
misago/categories/forms.py

@@ -9,7 +9,8 @@ from misago.core.forms import YesNoSwitch
 from misago.core.validators import validate_sluggable
 from misago.threads.threadtypes import trees_map
 
-from .models import THREADS_ROOT, Category, CategoryRole
+from . import THREADS_ROOT_NAME
+from .models import Category, CategoryRole
 
 
 class AdminCategoryFieldMixin(object):
@@ -17,7 +18,7 @@ class AdminCategoryFieldMixin(object):
         self.base_level = kwargs.pop('base_level', 1)
         kwargs['level_indicator'] = kwargs.get('level_indicator', '- - ')
 
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
         queryset = Category.objects.filter(tree_id=threads_tree_id)
         if not kwargs.pop('include_root', False):
             queryset = queryset.exclude(special_role="root_category")

+ 2 - 4
misago/categories/migrations/0002_default_categories.py

@@ -5,8 +5,6 @@ from django.db import migrations
 
 from misago.core.utils import slugify
 
-from misago.categories.models import PRIVATE_THREADS_ROOT, THREADS_ROOT
-
 
 _ = lambda s: s
 
@@ -15,7 +13,7 @@ def create_default_categories_tree(apps, schema_editor):
     Category = apps.get_model('misago_categories', 'Category')
 
     Category.objects.create(
-        special_role=PRIVATE_THREADS_ROOT,
+        special_role='private_threads',
         name='Private',
         slug='private',
         lft=1,
@@ -25,7 +23,7 @@ def create_default_categories_tree(apps, schema_editor):
     )
 
     root = Category.objects.create(
-        special_role=THREADS_ROOT,
+        special_role='root_category',
         name='Root',
         slug='root',
         lft=3,

+ 5 - 5
misago/categories/models.py

@@ -12,18 +12,18 @@ from misago.core.cache import cache
 from misago.core.utils import slugify
 from misago.threads.threadtypes import trees_map
 
+from . import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
+
 
 CACHE_NAME = 'misago_categories_tree'
-PRIVATE_THREADS_ROOT = 'private_threads'
-THREADS_ROOT = 'root_category'
 
 
 class CategoryManager(TreeManager):
     def private_threads(self):
-        return self.get_special(PRIVATE_THREADS_ROOT)
+        return self.get_special(PRIVATE_THREADS_ROOT_NAME)
 
     def root_category(self):
-        return self.get_special(THREADS_ROOT)
+        return self.get_special(THREADS_ROOT_NAME)
 
     def get_special(self, special_role):
         cache_name = '%s_%s' % (CACHE_NAME, special_role)
@@ -35,7 +35,7 @@ class CategoryManager(TreeManager):
         return special_category
 
     def all_categories(self, include_root=False):
-        tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
         queryset = self.filter(tree_id=tree_id)
         if not include_root:
             queryset = queryset.filter(level__gt=0)

+ 35 - 22
misago/categories/serializers.py

@@ -2,17 +2,13 @@ from rest_framework import serializers
 
 from django.urls import reverse
 
-from misago.api.serializers import MutableFields
+from misago.core.serializers import MutableFields
 from misago.core.utils import format_plaintext_for_html
 
 from .models import Category
 
 
-__all__ = [
-    'CategorySerializer',
-    'BasicCategorySerializer',
-    'CategoryWithPosterSerializer',
-]
+__all__ = ['CategorySerializer']
 
 
 def last_activity_detail(f):
@@ -22,16 +18,8 @@ def last_activity_detail(f):
         if not obj.last_thread_id:
             return None
 
-        try:
-            acl = obj.acl
-        except AttributeError:
-            return None
-            
-        tested_acls = (
-            acl.get('can_see'),
-            acl.get('can_browse'),
-            acl.get('can_see_all_threads'),
-        )
+        acl = self.get_acl(obj)
+        tested_acls = (acl.get('can_see'), acl.get('can_browse'), acl.get('can_see_all_threads'), )
 
         if not all(tested_acls):
             return None
@@ -46,6 +34,9 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     description = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
     subcategories = serializers.SerializerMethodField()
+    acl = serializers.SerializerMethodField()
+
+    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Category
@@ -64,9 +55,11 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
             'css_class',
             'is_read',
             'subcategories',
+            'acl',
             'level',
             'lft',
             'rght',
+            'url',
         ]
 
     def get_description(self, obj):
@@ -89,6 +82,12 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return []
 
+    def get_acl(self, obj):
+        try:
+            return obj.acl
+        except AttributeError:
+            return {}
+
     @last_activity_detail
     def get_last_poster(self, obj):
         if obj.last_poster_id:
@@ -104,6 +103,26 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
             }
         return None
 
+    def get_url(self, obj):
+        return {
+            'index': obj.get_absolute_url(),
+            'last_thread': self.get_last_thread_url(obj),
+            'last_thread_new': self.get_last_thread_new_url(obj),
+            'last_post': self.get_last_post_url(obj),
+        }
+
+    @last_activity_detail
+    def get_last_thread_url(self, obj):
+        return obj.get_last_thread_url()
+
+    @last_activity_detail
+    def get_last_thread_new_url(self, obj):
+        return obj.get_last_thread_new_url()
+
+    @last_activity_detail
+    def get_last_post_url(self, obj):
+        return obj.get_last_post_url()
+
 
 class CategoryWithPosterSerializer(CategorySerializer):
     last_poster = serializers.SerializerMethodField()
@@ -115,9 +134,3 @@ class CategoryWithPosterSerializer(CategorySerializer):
             return []
 
 CategoryWithPosterSerializer = CategoryWithPosterSerializer.extend_fields('last_poster')
-
-
-BasicCategorySerializer = CategorySerializer.subset_fields(
-    'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'level', 'lft', 'rght'
-)

+ 2 - 2
misago/categories/signals.py

@@ -1,6 +1,6 @@
 from django.dispatch import Signal, receiver
 
-from misago.users.signals import anonymize_user_content, username_changed
+from misago.users.signals import anonymize_user_data, username_changed
 
 from .models import Category
 
@@ -9,7 +9,7 @@ delete_category_content = Signal()
 move_category_content = Signal(providing_args=["new_category"])
 
 
-@receiver([anonymize_user_content, username_changed])
+@receiver([anonymize_user_data, username_changed])
 def update_usernames(sender, **kwargs):
     Category.objects.filter(last_poster=sender).update(
         last_poster_name=sender.username,

+ 3 - 2
misago/categories/tests/test_category_model.py

@@ -1,4 +1,5 @@
-from misago.categories.models import THREADS_ROOT, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.core.testutils import MisagoTestCase
 from misago.threads import testutils
 from misago.threads.threadtypes import trees_map
@@ -37,7 +38,7 @@ class CategoryManagerTests(MisagoTestCase):
         test_dict = Category.objects.get_categories_dict_from_db()
 
         for category in Category.objects.all():
-            threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+            threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
             if category.tree_id == threads_tree_id:
                 self.assertIn(category.id, test_dict)
             else:

+ 1 - 1
misago/categories/urls/api.py

@@ -1,5 +1,5 @@
-from misago.api.router import MisagoApiRouter
 from misago.categories.api import CategoryViewSet
+from misago.core.apirouter import MisagoApiRouter
 
 
 router = MisagoApiRouter()

+ 3 - 2
misago/categories/views/categoriesadmin.py

@@ -4,8 +4,9 @@ from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import version as acl_version
 from misago.admin.views import generic
+from misago.categories import THREADS_ROOT_NAME
 from misago.categories.forms import CategoryFormFactory, DeleteFormFactory
-from misago.categories.models import THREADS_ROOT, Category, RoleCategoryACL
+from misago.categories.models import Category, RoleCategoryACL
 from misago.threads.threadtypes import trees_map
 
 
@@ -18,7 +19,7 @@ class CategoryAdmin(generic.AdminBaseMixin):
     def get_target(self, kwargs):
         target = super(CategoryAdmin, self).get_target(kwargs)
 
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
 
         target_is_special = bool(target.special_role)
         target_not_in_categories_tree = target.tree_id != threads_tree_id

+ 3 - 2
misago/categories/views/categorieslist.py

@@ -8,8 +8,9 @@ from misago.categories.utils import get_categories_tree
 def categories(request):
     categories_tree = get_categories_tree(request.user, join_posters=True)
 
-    request.frontend_context['store'].update({
-        'categories': CategorySerializer(categories_tree, many=True).data,
+    request.frontend_context.update({
+        'CATEGORIES': CategorySerializer(categories_tree, many=True).data,
+        'CATEGORIES_API': reverse('misago:api:category-list'),
     })
 
     return render(request, 'misago/categories/list.html', {

+ 17 - 23
misago/conf/context_processors.py

@@ -8,22 +8,14 @@ from .gateway import settings as misago_settings  # noqa
 from .gateway import db_settings
 
 
-LOGO_URL = static(misago_settings.MISAGO_LOGO)
 BLANK_AVATAR_URL = static(misago_settings.MISAGO_BLANK_AVATAR)
 
-STYLE = misago_settings._MISAGO_STYLE_DEFAULT.copy()
-
-if misago_settings.MISAGO_STYLE:
-    STYLE.update(misago_settings.MISAGO_STYLE)
-
 
 def settings(request):
     return {
-        'MISAGO_STYLE': STYLE,
         'DEBUG': misago_settings.DEBUG,
         'LANGUAGE_CODE_SHORT': get_language()[:2],
         'misago_settings': db_settings,
-        'LOGO_URL': LOGO_URL,
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
         'LOGIN_REDIRECT_URL': misago_settings.LOGIN_REDIRECT_URL,
@@ -33,23 +25,25 @@ def settings(request):
 
 
 def preload_settings_json(request):
-    request.frontend_context['conf'].update(db_settings.get_public_settings())
-    request.frontend_context['conf'].update({
-        'csrf_cookie_name': misago_settings.CSRF_COOKIE_NAME,
-        'threads_on_index': misago_settings.MISAGO_THREADS_ON_INDEX,
-        'enable_delete_own_account': misago_settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT,
-        'social_auth_sites': get_enabled_social_auth_sites_list(),
-        'style': STYLE,
+    preloaded_settings = db_settings.get_public_settings()
+
+    preloaded_settings.update({
+        'LOGIN_API_URL': misago_settings.MISAGO_LOGIN_API_URL,
+        'LOGIN_REDIRECT_URL': reverse(misago_settings.LOGIN_REDIRECT_URL),
+        'LOGIN_URL': reverse(misago_settings.LOGIN_URL),
+        'LOGOUT_URL': reverse(misago_settings.LOGOUT_URL),
+        'SOCIAL_AUTH': get_enabled_social_auth_sites_list(),
     })
 
-    request.frontend_context['url'].update({
-        'index': reverse('misago:index'),
-        'blank_avatar': BLANK_AVATAR_URL,
-        'logo': LOGO_URL,
-        'login_redirect': reverse(misago_settings.LOGIN_REDIRECT_URL),
-        'login': reverse(misago_settings.LOGIN_URL),
-        'logout': reverse(misago_settings.LOGOUT_URL),
-        'static': misago_settings.STATIC_URL,
+    request.frontend_context.update({
+        'SETTINGS': preloaded_settings,
+        'MISAGO_PATH': reverse('misago:index'),
+        'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
+        'ENABLE_DOWNLOAD_OWN_DATA': misago_settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA,
+        'ENABLE_DELETE_OWN_ACCOUNT': misago_settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT,
+        'STATIC_URL': misago_settings.STATIC_URL,
+        'CSRF_COOKIE_NAME': misago_settings.CSRF_COOKIE_NAME,
+        'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
     })
 
     return {}

+ 40 - 18
misago/conf/defaults.py

@@ -7,6 +7,11 @@ instead of Django's `django.conf.settings`.
 """
 
 
+# Complete HTTP address of your Misago installation
+
+MISAGO_ADDRESS = None
+
+
 # Permissions system extensions
 # https://misago.readthedocs.io/en/latest/developers/acls.html#extending-permissions-system
 
@@ -30,8 +35,35 @@ MISAGO_ACL_EXTENSIONS = [
 MISAGO_ANONYMOUS_USERNAME = "Ghost"
 
 
-# Allow users to delete their own accounts?
-# Providing such feature is required by EU law from entities that process europeans personal data.
+# Allow users to download their personal data
+# Enables users to learn what data about them is being held by the site without having to contact
+# site's administrators.
+
+MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
+
+# Number of hours for which user data should be available for download.
+# When data download is marked as expired, data archive associated with it is deleted.
+
+MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS = 48
+
+# Path to the directory that Misago should use to prepare user data downloads.
+# Should not be accessible from internet.
+
+MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = None
+
+
+# Automatically delete new user accounts that weren't activated in specified time
+# If you rely on admin review of new registrations, make this period long, disable
+# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
+# keep it short to give users a chance to retry on their own after few days pass.s
+
+MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 0
+
+
+# Allow users to delete their accounts
+# Lets users delete their own account on the site without having to contact site administrators.
+# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
+# data about user left behind after user is deleted.
 
 MISAGO_ENABLE_DELETE_OWN_ACCOUNT = False
 
@@ -201,22 +233,6 @@ MISAGO_AVATARS_SIZES = [400, 200, 150, 100, 64, 50, 30]
 MISAGO_BLANK_AVATAR = 'blank-avatar.png'
 
 
-# Path to default logo image used in navbar.
-
-MISAGO_LOGO = 'logo.png'
-
-
-# Empty style options and defaults
-
-MISAGO_STYLE = {}
-
-# Default style settings that are updated with user-entered MISAGO_STYLE
-
-_MISAGO_STYLE_DEFAULT = {
-    'navbar': 'navbar-light bg-light',
-}
-
-
 # Threads lists pagination settings
 
 MISAGO_THREADS_PER_PAGE = 25
@@ -275,6 +291,12 @@ MISAGO_RANKING_LENGTH = 30
 MISAGO_RANKING_SIZE = 50
 
 
+# Specifies the number of days that IP addresses are stored in the database before removing.
+# Change this setting to None to never remove old IP addresses.
+
+MISAGO_IP_STORE_TIME = None
+
+
 # Controls number of users displayed on single page
 
 MISAGO_USERS_PER_PAGE = 12

+ 1 - 1
misago/conf/tests/test_context_processors.py

@@ -24,4 +24,4 @@ class ContextProcessorsTests(TestCase):
         """site configuration is preloaded by middleware"""
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertContains(response, '"conf": {"')
+        self.assertContains(response, '"SETTINGS": {"')

+ 116 - 0
misago/core/apipatch.py

@@ -0,0 +1,116 @@
+from rest_framework.response import Response
+
+from django.core.exceptions import PermissionDenied
+from django.db import transaction
+from django.http import Http404
+
+
+ALLOWED_OPS = ('add', 'remove', 'replace')
+
+
+class InvalidAction(Exception):
+    pass
+
+
+class ApiPatch(object):
+    def __init__(self):
+        self._actions = []
+
+    def add(self, path, handler):
+        self._actions.append({
+            'op': 'add',
+            'path': path,
+            'handler': handler,
+        })
+
+    def remove(self, path, handler):
+        self._actions.append({
+            'op': 'remove',
+            'path': path,
+            'handler': handler,
+        })
+
+    def replace(self, path, handler):
+        self._actions.append({
+            'op': 'replace',
+            'path': path,
+            'handler': handler,
+        })
+
+    def dispatch(self, request, target):
+        if not isinstance(request.data, list):
+            return Response({
+                'detail': "PATCH request should be list of operations",
+            }, status=400)
+
+        detail = []
+        is_errored = False
+
+        patch = {'id': target.pk}
+        for action in request.data:
+            try:
+                self.validate_action(action)
+                self.dispatch_action(patch, request, target, action)
+                detail.append('ok')
+            except Http404:
+                is_errored = True
+                detail.append('NOT FOUND')
+                break
+            except (InvalidAction, PermissionDenied) as e:
+                is_errored = True
+                detail.append(e.args[0])
+                break
+
+        patch['detail'] = detail
+        if is_errored:
+            return Response(patch, status=400)
+        else:
+            return Response(patch)
+
+    def dispatch_bulk(self, request, targets):
+        is_errored = False
+        result = []
+
+        for target in targets:
+            detail = []
+
+            patch = {'id': target.pk}
+            for action in request.data['ops']:
+                try:
+                    self.validate_action(action)
+                    self.dispatch_action(patch, request, target, action)
+                except Http404:
+                    is_errored = True
+                    detail.append('NOT FOUND')
+                    break
+                except (InvalidAction, PermissionDenied) as e:
+                    is_errored = True
+                    detail.append(e.args[0])
+                    break
+            if detail:
+                patch['detail'] = detail
+            result.append(patch)
+
+        if is_errored:
+            return Response(result, status=400)
+        else:
+            return Response(result)
+
+    def validate_action(self, action):
+        if not action.get('op'):
+            raise InvalidAction(u"undefined op")
+
+        if action.get('op') not in ALLOWED_OPS:
+            raise InvalidAction(u'"%s" op is unsupported' % action.get('op'))
+
+        if not action.get('path'):
+            raise InvalidAction(u'"%s" op has to specify path' % action.get('op'))
+
+        if 'value' not in action:
+            raise InvalidAction(u'"%s" op has to specify value' % action.get('op'))
+
+    def dispatch_action(self, patch, request, target, action):
+        for handler in self._actions:
+            if action['op'] == handler['op'] and action['path'] == handler['path']:
+                with transaction.atomic():
+                    patch.update(handler['handler'](request, target, action['value']))

+ 0 - 0
misago/api/router.py → misago/core/apirouter.py


+ 25 - 0
misago/core/context_processors.py

@@ -21,7 +21,32 @@ def site_address(request):
     }
 
 
+def current_link(request):
+    if not request.resolver_match or request.frontend_context.get('CURRENT_LINK'):
+        return {}
+
+    url_name = request.resolver_match.url_name
+    if request.resolver_match.namespaces:
+        namespaces = ':'.join(request.resolver_match.namespaces)
+        link_name = '{}:{}'.format(namespaces, url_name)
+    else:
+        link_name = url_name
+
+    request.frontend_context.update({'CURRENT_LINK': link_name})
+
+    return {}
+
+
 def momentjs_locale(request):
     return {
         'MOMENTJS_LOCALE_URL': get_locale_url(get_language()),
     }
+
+
+def frontend_context(request):
+    if request.include_frontend_context:
+        return {
+            'frontend_context': request.frontend_context,
+        }
+    else:
+        return {}

+ 8 - 5
misago/core/errorpages.py

@@ -18,6 +18,10 @@ def _ajax_error(code, exception=None, default_message=None):
 
 @admin_error_page
 def _error_page(request, code, exception=None, default_message=None):
+    request.frontend_context.update({
+        'CURRENT_LINK': 'misago:error-%s' % code,
+    })
+
     return render(
         request, 'misago/errorpages/%s.html' % code, {
             'message': get_exception_message(exception, default_message),
@@ -28,12 +32,11 @@ def _error_page(request, code, exception=None, default_message=None):
 def banned(request, exception):
     ban = exception.ban
 
-    request.frontend_context['store'].update({
-        'error': {
-            'ban': ban.get_serialized_message(),
-        },
+    request.frontend_context.update({
+        'MESSAGE': ban.get_serialized_message(),
+        'CURRENT_LINK': 'misago:error-banned',
     })
-    
+
     return render(
         request, 'misago/errorpages/banned.html', {
             'ban': ban,

+ 17 - 0
misago/core/exceptionhandler.py

@@ -1,3 +1,5 @@
+from rest_framework.views import exception_handler as rest_exception_handler
+
 from django.core.exceptions import PermissionDenied
 from django.http import Http404, HttpResponsePermanentRedirect, JsonResponse
 from django.urls import reverse
@@ -94,3 +96,18 @@ def get_exception_handler(exception):
 def handle_misago_exception(request, exception):
     handler = get_exception_handler(exception)
     return handler(request, exception)
+
+
+def handle_api_exception(exception, context):
+    response = rest_exception_handler(exception, context)
+    if response:
+        if isinstance(exception, Banned):
+            response.data['ban'] = exception.ban.get_serialized_message()
+        elif isinstance(exception, PermissionDenied):
+            try:
+                response.data['detail'] = exception.args[0]
+            except IndexError:
+                pass
+        return response
+    else:
+        return None

+ 25 - 11
misago/core/mail.py

@@ -1,15 +1,29 @@
 from django.core import mail as djmail
 from django.template.loader import render_to_string
+from django.utils.translation import get_language
 
+from misago.conf import db_settings, settings
 
-def build_mail(request, recipient, subject, template, context=None):
-    context = context or {}
-    context['sender'] = request.user
-    context['recipient'] = recipient
-    context['subject'] = subject
+from .utils import get_host_from_address
 
-    message_plain = render_to_string('%s.txt' % template, context, request=request)
-    message_html = render_to_string('%s.html' % template, context, request=request)
+
+def build_mail(recipient, subject, template, sender=None, context=None):
+    context = context.copy() if context else {}
+    context.update({
+        'SITE_ADDRESS': settings.MISAGO_ADDRESS,
+        'SITE_HOST': get_host_from_address(settings.MISAGO_ADDRESS),
+        'LANGUAGE_CODE': get_language()[:2],
+        'LOGIN_URL': settings.LOGIN_URL,
+
+        'misago_settings': db_settings,
+
+        'user': recipient,
+        'sender': sender,
+        'subject': subject,
+    })
+
+    message_plain = render_to_string('%s.txt' % template, context)
+    message_html = render_to_string('%s.html' % template, context)
 
     message = djmail.EmailMultiAlternatives(subject, message_plain, to=[recipient.email])
     message.attach_alternative(message_html, "text/html")
@@ -17,16 +31,16 @@ def build_mail(request, recipient, subject, template, context=None):
     return message
 
 
-def mail_user(request, recipient, subject, template, context=None):
-    message = build_mail(request, recipient, subject, template, context)
+def mail_user(recipient, subject, template, sender=None, context=None):
+    message = build_mail(recipient, subject, template, sender, context)
     message.send()
 
 
-def mail_users(request, recipients, subject, template, context=None):
+def mail_users(recipients, subject, template, sender=None, context=None):
     messages = []
 
     for recipient in recipients:
-        messages.append(build_mail(request, recipient, subject, template, context))
+        messages.append(build_mail(recipient, subject, template, sender, context))
 
     if messages:
         send_messages(messages)

+ 7 - 0
misago/core/middleware/frontendcontext.py

@@ -0,0 +1,7 @@
+from django.utils.deprecation import MiddlewareMixin
+
+
+class FrontendContextMiddleware(MiddlewareMixin):
+    def process_request(self, request):
+        request.include_frontend_context = True
+        request.frontend_context = {}

+ 0 - 21
misago/core/migrations/0003_remove_forum_branding_display_setting.py

@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.6 on 2017-11-03 23:12
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-def remove_forum_branding_display_setting(apps, schema_editor):
-    Setting = apps.get_model('misago_conf', 'Setting')
-    Setting.objects.filter(setting='forum_branding_display').delete()
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('misago_core', '0002_basic_settings'),
-    ]
-
-    operations = [
-        migrations.RunPython(remove_forum_branding_display_setting),
-    ]

+ 0 - 0
misago/api/rest_permissions.py → misago/core/rest_permissions.py


+ 0 - 0
misago/api/serializers.py → misago/core/serializers.py


+ 19 - 0
misago/core/templatetags/misago_absoluteurl.py

@@ -0,0 +1,19 @@
+from django import template
+from django.urls import reverse
+
+from misago.conf import settings
+
+register = template.Library()
+
+
+@register.simple_tag
+def absoluteurl(url_or_name, *args, **kwargs):
+    if not settings.MISAGO_ADDRESS:
+        return None
+
+    absolute_url_prefix = settings.MISAGO_ADDRESS.rstrip('/')
+
+    if '/' not in url_or_name:
+        url_or_name = reverse(url_or_name, args=args, kwargs=kwargs)
+    
+    return u'{}{}'.format(absolute_url_prefix, url_or_name)

+ 1 - 3
misago/core/testproject/urls.py

@@ -6,7 +6,7 @@ from django.views.decorators.cache import cache_page
 from django.views.decorators.http import last_modified
 from django.views.i18n import JavaScriptCatalog
 
-from misago.admin.forms import AdminAuthenticationForm
+from misago.users.forms.auth import AdminAuthenticationForm
 
 from . import views
 
@@ -29,8 +29,6 @@ urlpatterns = [
         ),
         name='django-i18n'
     ),
-    url(r'^forum/test-mail-user/$', views.test_mail_user, name='test-mail-user'),
-    url(r'^forum/test-mail-users/$', views.test_mail_users, name='test-mail-users'),
     url(r'^forum/test-pagination/$', views.test_pagination, name='test-pagination'),
     url(
         r'^forum/test-pagination/(?P<page>[1-9][0-9]*)/$',

+ 1 - 1
misago/core/testproject/urlswitherrorhandlers.py

@@ -1,4 +1,4 @@
-from .urls import *  # pylint: disable=unused-wildcard-import
+from .urls import *
 
 
 handler403 = 'misago.core.testproject.views.mock_custom_403_error_page'

+ 1 - 20
misago/core/testproject/views.py

@@ -1,12 +1,11 @@
 from rest_framework.decorators import api_view
 
-from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.http import Http404, HttpResponse
 from social_core.exceptions import AuthFailed, NotAllowedToDisconnect, WrongBackend
 from social_core.backends.github import GithubOAuth2
 
-from misago.core import errorpages, mail
+from misago.core import errorpages
 from misago.core.decorators import require_POST
 from misago.core.exceptions import Banned, SocialAuthBanned, SocialAuthFailed
 from misago.core.shortcuts import paginate, paginated_response, validate_slug
@@ -17,24 +16,6 @@ from .models import Model
 from .serializers import MockSerializer
 
 
-UserModel = get_user_model()
-
-
-def test_mail_user(request):
-    test_user = UserModel.objects.all().first()
-    mail.mail_user(request, test_user, "Misago Test Mail", "misago/emails/base")
-
-    return HttpResponse("Mailed user!")
-
-
-def test_mail_users(request):
-    mail.mail_users(
-        request, UserModel.objects.iterator(), "Misago Test Spam", "misago/emails/base"
-    )
-
-    return HttpResponse("Mailed users!")
-
-
 def test_pagination(request, page=None):
     items = range(15)
     page = paginate(items, page, 5)

+ 323 - 0
misago/core/tests/test_apipatch.py

@@ -0,0 +1,323 @@
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.test import TestCase
+
+from misago.core.apipatch import ApiPatch, InvalidAction
+
+
+class MockRequest(object):
+    def __init__(self, data=None):
+        self.data = data
+
+
+class MockObject(object):
+    def __init__(self, pk):
+        self.id = pk
+        self.pk = pk
+
+
+class ApiPatchTests(TestCase):
+    def test_add(self):
+        """add method adds function to patch object"""
+        patch = ApiPatch()
+
+        def mock_function():
+            pass
+
+        patch.add('test-add', mock_function)
+
+        self.assertEqual(len(patch._actions), 1)
+        self.assertEqual(patch._actions[0]['op'], 'add')
+        self.assertEqual(patch._actions[0]['path'], 'test-add')
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
+
+    def test_remove(self):
+        """remove method adds function to patch object"""
+        patch = ApiPatch()
+
+        def mock_function():
+            pass
+
+        patch.remove('test-remove', mock_function)
+
+        self.assertEqual(len(patch._actions), 1)
+        self.assertEqual(patch._actions[0]['op'], 'remove')
+        self.assertEqual(patch._actions[0]['path'], 'test-remove')
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
+
+    def test_replace(self):
+        """replace method adds function to patch object"""
+        patch = ApiPatch()
+
+        def mock_function():
+            pass
+
+        patch.replace('test-replace', mock_function)
+
+        self.assertEqual(len(patch._actions), 1)
+        self.assertEqual(patch._actions[0]['op'], 'replace')
+        self.assertEqual(patch._actions[0]['path'], 'test-replace')
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
+
+    def test_validate_action(self):
+        """validate_action method validates action dict"""
+        patch = ApiPatch()
+
+        VALID_ACTIONS = [
+            {
+                'op': 'add',
+                'path': 'test',
+                'value': 42
+            },
+            {
+                'op': 'remove',
+                'path': 'other-test',
+                'value': 'Lorem'
+            },
+            {
+                'op': 'replace',
+                'path': 'false-test',
+                'value': None
+            },
+        ]
+
+        for action in VALID_ACTIONS:
+            patch.validate_action(action)
+
+        # undefined op
+        UNSUPPORTED_ACTIONS = ({}, {'op': ''}, {'no': 'op'}, )
+
+        for action in UNSUPPORTED_ACTIONS:
+            try:
+                patch.validate_action(action)
+            except InvalidAction as e:
+                self.assertEqual(e.args[0], u"undefined op")
+
+        # unsupported op
+        try:
+            patch.validate_action({'op': 'nope'})
+        except InvalidAction as e:
+            self.assertEqual(e.args[0], u'"nope" op is unsupported')
+
+        # op lacking patch
+        try:
+            patch.validate_action({'op': 'add'})
+        except InvalidAction as e:
+            self.assertEqual(e.args[0], u'"add" op has to specify path')
+
+        # op lacking value
+        try:
+            patch.validate_action({
+                'op': 'add',
+                'path': 'yolo',
+            })
+        except InvalidAction as e:
+            self.assertEqual(e.args[0], u'"add" op has to specify value')
+
+        # empty value is allowed
+        try:
+            patch.validate_action({
+                'op': 'add',
+                'path': 'yolo',
+                'value': '',
+            })
+        except InvalidAction as e:
+            self.assertEqual(e.args[0], u'"add" op has to specify value')
+
+    def test_dispatch_action(self):
+        """dispatch_action calls specified actions"""
+        patch = ApiPatch()
+
+        mock_target = MockObject(13)
+
+        def action_a(request, target, value):
+            self.assertEqual(request, 'request')
+            self.assertEqual(target, mock_target)
+            return {'a': value * 2, 'b': 111}
+
+        patch.replace('abc', action_a)
+
+        def action_b(request, target, value):
+            self.assertEqual(request, 'request')
+            self.assertEqual(target, mock_target)
+            return {'b': value * 10}
+
+        patch.replace('abc', action_b)
+
+        def action_fail(request, target, value):
+            self.fail("unrequired action was called")
+
+        patch.add('c', action_fail)
+        patch.remove('c', action_fail)
+        patch.replace('c', action_fail)
+
+        patch_dict = {'id': 123}
+
+        patch.dispatch_action(
+            patch_dict, 'request', mock_target, {
+                'op': 'replace',
+                'path': 'abc',
+                'value': 5,
+            }
+        )
+
+        self.assertEqual(len(patch_dict), 3)
+        self.assertEqual(patch_dict['id'], 123)
+        self.assertEqual(patch_dict['a'], 10)
+        self.assertEqual(patch_dict['b'], 50)
+
+    def test_dispatch(self):
+        """dispatch calls actions and returns response"""
+        patch = ApiPatch()
+
+        def action_error(request, target, value):
+            if value == '404':
+                raise Http404()
+            if value == 'perm':
+                raise PermissionDenied("yo ain't doing that!")
+
+        patch.replace('error', action_error)
+
+        def action_mutate(request, target, value):
+            return {'value': value * 2}
+
+        patch.replace('mutate', action_mutate)
+
+        # dispatch requires list as an argument
+        response = patch.dispatch(MockRequest({}), {})
+        self.assertEqual(response.status_code, 400)
+
+        self.assertEqual(response.data['detail'], "PATCH request should be list of operations")
+
+        # valid dispatch
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
+
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(len(response.data['detail']), 3)
+        self.assertEqual(response.data['detail'][0], 'ok')
+        self.assertEqual(response.data['detail'][1], 'ok')
+        self.assertEqual(response.data['detail'][2], 'ok')
+        self.assertEqual(response.data['id'], 13)
+        self.assertEqual(response.data['value'], 14)
+
+        # invalid action in dispatch
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
+
+        self.assertEqual(response.status_code, 400)
+
+        self.assertEqual(len(response.data['detail']), 3)
+        self.assertEqual(response.data['detail'][0], 'ok')
+        self.assertEqual(response.data['detail'][1], 'ok')
+        self.assertEqual(response.data['detail'][2], '"replace" op has to specify path')
+        self.assertEqual(response.data['id'], 13)
+        self.assertEqual(response.data['value'], 12)
+
+        # action in dispatch raised 404
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'error',
+                    'value': '404',
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
+
+        self.assertEqual(response.status_code, 400)
+
+        self.assertEqual(len(response.data['detail']), 2)
+        self.assertEqual(response.data['detail'][0], 'ok')
+        self.assertEqual(response.data['detail'][1], "NOT FOUND")
+        self.assertEqual(response.data['id'], 13)
+        self.assertEqual(response.data['value'], 4)
+
+        # action in dispatch raised perm denied
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 9,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'error',
+                    'value': 'perm',
+                },
+            ]), MockObject(13)
+        )
+
+        self.assertEqual(response.status_code, 400)
+
+        self.assertEqual(len(response.data['detail']), 4)
+        self.assertEqual(response.data['detail'][0], 'ok')
+        self.assertEqual(response.data['detail'][1], 'ok')
+        self.assertEqual(response.data['detail'][2], 'ok')
+        self.assertEqual(response.data['detail'][3], "yo ain't doing that!")
+        self.assertEqual(response.data['id'], 13)
+        self.assertEqual(response.data['value'], 18)

+ 19 - 0
misago/core/tests/test_context_processors.py

@@ -79,3 +79,22 @@ class SiteAddressTests(TestCase):
                 'SITE_PROTOCOL': 'https',
             }
         )
+
+
+class FrontendContextTests(TestCase):
+    def test_frontend_context(self):
+        """frontend_context is available in templates"""
+        mock_request = MockRequest(False, 'somewhere.com')
+        mock_request.include_frontend_context = True
+        mock_request.frontend_context = {'someValue': 'Something'}
+
+        self.assertEqual(
+            context_processors.frontend_context(mock_request), {
+                'frontend_context': {
+                    'someValue': 'Something',
+                },
+            }
+        )
+
+        mock_request.include_frontend_context = False
+        self.assertEqual(context_processors.frontend_context(mock_request), {})

+ 7 - 17
misago/core/tests/test_errorpages.py

@@ -22,21 +22,26 @@ class ErrorPageViewsTests(TestCase):
     def test_banned_returns_403(self):
         """banned error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-banned'))
+        self.assertContains(response, "misago:error-banned", status_code=403)
         self.assertContains(response, "<p>Banned for test!</p>", status_code=403)
+        self.assertContains(response, encode_json_html("<p>Banned for test!</p>"), status_code=403)
 
     def test_permission_denied_returns_403(self):
         """permission_denied error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-403'))
+        self.assertContains(response, "misago:error-403", status_code=403)
         self.assertContains(response, "Page not available", status_code=403)
 
     def test_page_not_found_returns_404(self):
         """page_not_found error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-404'))
+        self.assertContains(response, "misago:error-404", status_code=404)
         self.assertContains(response, "Page not found", status_code=404)
 
     def test_not_allowed_returns_405(self):
         """not allowed error page has no showstoppers"""
         response = self.client.get(reverse('raise-misago-405'))
+        self.assertContains(response, "misago:error-405", status_code=405)
         self.assertContains(response, "Wrong way", status_code=405)
 
     def test_social_auth_failed_returns_403(self):
@@ -80,31 +85,18 @@ class CustomErrorPagesTests(TestCase):
         self.misago_request.include_frontend_context = True
         self.site_request.include_frontend_context = True
 
-        self.misago_request.frontend_context = {
-            'auth': {},
-            'conf': {},
-            'store': {},
-            'url': {},
-        }
-
-        self.site_request.frontend_context = {
-            'auth': {},
-            'conf': {},
-            'store': {},
-            'url': {},
-        }
+        self.misago_request.frontend_context = {}
+        self.site_request.frontend_context = {}
 
     def test_shared_403_decorator(self):
         """shared_403_decorator calls correct error handler"""
         response = self.client.get(reverse('raise-misago-403'))
         self.assertEqual(response.status_code, 403)
-
         response = self.client.get(reverse('raise-403'))
         self.assertContains(response, "Custom 403", status_code=403)
 
         response = mock_custom_403_error_page(self.misago_request, PermissionDenied())
         self.assertNotContains(response, "Custom 403", status_code=403)
-
         response = mock_custom_403_error_page(self.site_request, PermissionDenied())
         self.assertContains(response, "Custom 403", status_code=403)
 
@@ -112,12 +104,10 @@ class CustomErrorPagesTests(TestCase):
         """shared_404_decorator calls correct error handler"""
         response = self.client.get(reverse('raise-misago-404'))
         self.assertEqual(response.status_code, 404)
-
         response = self.client.get(reverse('raise-404'))
         self.assertContains(response, "Custom 404", status_code=404)
 
         response = mock_custom_404_error_page(self.misago_request, Http404())
         self.assertNotContains(response, "Custom 404", status_code=404)
-
         response = mock_custom_404_error_page(self.site_request, Http404())
         self.assertContains(response, "Custom 404", status_code=404)

+ 1 - 6
misago/core/tests/test_exceptionhandler_middleware.py

@@ -12,12 +12,7 @@ class ExceptionHandlerMiddlewareTests(TestCase):
         self.request = RequestFactory().get(reverse('misago:index'))
         self.request.user = AnonymousUser()
         self.request.include_frontend_context = True
-        self.request.frontend_context = {
-            'auth': {},
-            'conf': {},
-            'store': {},
-            'url': {},
-        }
+        self.request.frontend_context = {}
 
     def test_middleware_returns_response_for_supported_exception(self):
         """Middleware returns HttpResponse for supported exception"""

+ 38 - 0
misago/core/tests/test_exceptionhandlers.py

@@ -1,7 +1,11 @@
 from django.core import exceptions as django_exceptions
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
 from django.test import TestCase
 
 from misago.core import exceptionhandler
+from misago.core.exceptions import Banned
+from misago.users.models import Ban
 
 
 INVALID_EXCEPTIONS = [
@@ -42,3 +46,37 @@ class GetExceptionHandlerTests(TestCase):
         for exception in INVALID_EXCEPTIONS:
             with self.assertRaises(ValueError):
                 exceptionhandler.get_exception_handler(exception())
+
+
+class HandleAPIExceptionTests(TestCase):
+    def test_banned(self):
+        """banned exception is correctly handled"""
+        ban = Ban(user_message="This is test ban!")
+
+        response = exceptionhandler.handle_api_exception(Banned(ban), None)
+
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.data['ban']['message']['html'], "<p>This is test ban!</p>")
+
+    def test_permission_denied(self):
+        """permission denied exception is correctly handled"""
+        response = exceptionhandler.handle_api_exception(PermissionDenied(), None)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.data['detail'], "Permission denied.")
+
+    def test_permission_message_denied(self):
+        """permission denied with message is correctly handled"""
+        exception = PermissionDenied("You shall not pass!")
+        response = exceptionhandler.handle_api_exception(exception, None)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.data['detail'], "You shall not pass!")
+
+    def test_unhandled_exception(self):
+        """our exception handler is not interrupting other exceptions"""
+        for exception in INVALID_EXCEPTIONS:
+            response = exceptionhandler.handle_api_exception(exception(), None)
+            self.assertIsNone(response)
+
+        response = exceptionhandler.handle_api_exception(Http404(), None)
+        self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.data['detail'], "Not found.")

+ 2 - 8
misago/api/tests/test_frontendcontext_middleware.py → misago/core/tests/test_frontendcontext_middleware.py

@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from misago.api.middleware import FrontendContextMiddleware
+from misago.core.middleware.frontendcontext import FrontendContextMiddleware
 
 
 class MockRequest(object):
@@ -13,10 +13,4 @@ class FrontendContextMiddlewareTests(TestCase):
         request = MockRequest()
 
         FrontendContextMiddleware().process_request(request)
-        self.assertEqual(request.frontend_context, {
-            'api': '/api/',
-            'auth': {},
-            'conf': {},
-            'store': {},
-            'url': {}
-        })
+        self.assertEqual(request.frontend_context, {})

+ 8 - 9
misago/core/tests/test_mailer.py → misago/core/tests/test_mail.py

@@ -1,20 +1,20 @@
 from django.contrib.auth import get_user_model
 from django.core import mail
-from django.test import TestCase, override_settings
+from django.test import TestCase
 from django.urls import reverse
 
+from misago.core.mail import mail_user, mail_users
+
 
 UserModel = get_user_model()
 
 
-@override_settings(ROOT_URLCONF='misago.core.testproject.urls')
-class MisagoMailerTests(TestCase):
+class MailTests(TestCase):
     def test_mail_user(self):
         """mail_user sets message in backend"""
         user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
 
-        response = self.client.get(reverse('test-mail-user'))
-        self.assertEqual(response.status_code, 200)
+        mail_user(user, "Misago Test Mail", "misago/emails/base")
 
         self.assertEqual(mail.outbox[0].subject, "Misago Test Mail")
 
@@ -26,16 +26,15 @@ class MisagoMailerTests(TestCase):
 
     def test_mail_users(self):
         """mail_users sets messages in backend"""
-        test_users = (
+        test_users = [
             UserModel.objects.create_user('Alpha', 'alpha@test.com', 'pass123'),
             UserModel.objects.create_user('Beta', 'beta@test.com', 'pass123'),
             UserModel.objects.create_user('Niner', 'niner@test.com', 'pass123'),
             UserModel.objects.create_user('Foxtrot', 'foxtrot@test.com', 'pass123'),
             UserModel.objects.create_user('Uniform', 'uniform@test.com', 'pass123'),
-        )
+        ]
 
-        response = self.client.get(reverse('test-mail-users'))
-        self.assertEqual(response.status_code, 200)
+        mail_users(test_users, "Misago Test Spam", "misago/emails/base")
 
         spams_sent = 0
         for message in mail.outbox:

+ 1 - 1
misago/api/tests/test_serializers.py → misago/core/tests/test_serializers.py

@@ -2,8 +2,8 @@ from rest_framework import serializers
 
 from django.test import TestCase
 
-from misago.api.serializers import MutableFields
 from misago.categories.models import Category
+from misago.core.serializers import MutableFields
 from misago.threads import testutils
 from misago.threads.models import Thread
 

+ 27 - 1
misago/core/tests/test_templatetags.py

@@ -1,8 +1,34 @@
 from django import forms
 from django.template import Context, Template, TemplateSyntaxError
-from django.test import TestCase
+from django.test import TestCase, override_settings
 
 from misago.core.templatetags import misago_batch
+from misago.core.templatetags.misago_absoluteurl import absoluteurl
+
+
+TEST_ADDRESS = 'https://testsite.com/'
+
+
+class AbsoluteUrlTests(TestCase):
+    @override_settings(MISAGO_ADDRESS=None)
+    def test_address_is_none(self):
+        """template tag returns null if address setting is not filled"""
+        result = absoluteurl('misago:index')
+        self.assertIsNone(result)
+
+    
+    @override_settings(MISAGO_ADDRESS=TEST_ADDRESS)
+    def test_prefix_url(self):
+        """template tag prefixes already reversed url"""
+        result = absoluteurl('/')
+        self.assertEqual(result, TEST_ADDRESS)
+
+    
+    @override_settings(MISAGO_ADDRESS=TEST_ADDRESS)
+    def test_rprefix_url_name(self):
+        """template tag reverses url name and prefixes it"""
+        result = absoluteurl('misago:index')
+        self.assertEqual(result, TEST_ADDRESS)
 
 
 class CaptureTests(TestCase):

+ 67 - 1
misago/core/tests/test_utils.py

@@ -9,7 +9,7 @@ from django.utils import six
 
 from misago.core.utils import (
     clean_return_path, format_plaintext_for_html, is_referer_local, is_request_to_misago,
-    parse_iso8601_string, slugify, get_exception_message)
+    parse_iso8601_string, slugify, get_exception_message, clean_ids_list, get_host_from_address)
 
 
 class IsRequestToMisagoTests(TestCase):
@@ -264,3 +264,69 @@ class GetExceptionMessageTests(TestCase):
 
         message = get_exception_message(default_message='Lorem Ipsum')
         self.assertEqual(message, 'Lorem Ipsum')
+
+
+class CleanIdsListTests(TestCase):
+    def test_valid_list(self):
+        """list of valid ids is cleaned"""
+        self.assertEqual(clean_ids_list(['1', 3, '42'], None), [1, 3, 42])
+
+    def test_empty_list(self):
+        """empty list passes validation"""
+        self.assertEqual(clean_ids_list([], None), [])
+
+    def test_string_list(self):
+        """string list passes validation"""
+        self.assertEqual(clean_ids_list('1234', None), [1, 2, 3, 4])
+
+    def test_message(self):
+        """utility uses passed message for exception"""
+        with self.assertRaisesMessage(PermissionDenied, "Test error message!"):
+            clean_ids_list(None, "Test error message!")
+
+    def test_invalid_inputs(self):
+        """utility raises exception for invalid inputs"""
+        INVALID_INPUTS = (
+            None,
+            'abc',
+            [None],
+            [1, 2, 'a', 4],
+            [1, None, 3],
+            {1: 2, 'a': 4},
+        )
+
+        for invalid_input in INVALID_INPUTS:
+            with self.assertRaisesMessage(PermissionDenied, "Test error message!"):
+                clean_ids_list(invalid_input, "Test error message!")
+
+
+class GetHostFromAddressTests(TestCase):
+    def test_none(self):
+        """get_host_from_address returns None for None"""
+        result = get_host_from_address(None)
+        self.assertIsNone(result)
+
+    def test_empty_string(self):
+        """get_host_from_address returns None for empty string"""
+        result = get_host_from_address('')
+        self.assertIsNone(result)
+
+    def test_hostname(self):
+        """get_host_from_address returns hostname unchanged"""
+        result = get_host_from_address('hostname')
+        self.assertEqual(result, 'hostname')
+        
+    def test_hostname_with_trailing_slash(self):
+        """get_host_from_address returns hostname for hostname with trailing slash"""
+        result = get_host_from_address('//hostname')
+        self.assertEqual(result, 'hostname')
+
+    def test_hostname_with_port(self):
+        """get_host_from_address returns hostname for hostname with port"""
+        result = get_host_from_address('hostname:8888')
+        self.assertEqual(result, 'hostname')
+        
+    def test_hostname_with_port_path_and_protocol(self):
+        """get_host_from_address returns hostname for hostname with port and path"""
+        result = get_host_from_address('https://hostname:8888/somewhere/else/')
+        self.assertEqual(result, 'hostname')

+ 26 - 11
misago/core/utils.py

@@ -9,7 +9,6 @@ from django.utils.encoding import force_text
 from django.utils.module_loading import import_string
 
 
-ANONYMOUS_IP = '0.0.0.0'
 MISAGO_SLUGIFY = getattr(settings, 'MISAGO_SLUGIFY', 'misago.core.slugify.default')
 
 slugify = import_string(MISAGO_SLUGIFY)
@@ -56,16 +55,6 @@ def parse_iso8601_string(value):
     return timezone.make_aware(parsed_value, tz_correction)
 
 
-def serialize_datetime(value):
-    if value is None:
-        return None
-        
-    value = value.isoformat()
-    if value.endswith('+00:00'):
-        value = value[:-6] + 'Z'
-    return value
-
-
 def hide_post_parameters(request):
     """
     Mark request as having sensitive parameters
@@ -158,3 +147,29 @@ def get_exception_message(exception=None, default_message=None):
         return exception.args[0]
     except IndexError:
         return default_message
+
+
+def clean_ids_list(ids_list, error_message):
+    try:
+        return list(map(int, ids_list))
+    except (ValueError, TypeError):
+        raise PermissionDenied(error_message)
+
+
+def get_host_from_address(address):
+    if not address:
+        return None
+
+    if address.lower().startswith('https://'):
+        address = address[8:]
+    if address.lower().startswith('http://'):
+        address = address[7:]
+    if address[0] == '/':
+        address = address.lstrip('/')
+    if '/' in address:
+        address = address.split('/')[0] or address
+    if ':' in address:
+        address = address.split(':')[0] or address
+
+    return address
+    

+ 0 - 2
misago/faker/management/commands/createfakethreads.py

@@ -85,7 +85,6 @@ class Command(BaseCommand):
                     thread=thread,
                     poster=user,
                     poster_name=user.username,
-                    poster_ip=fake.ipv4(),
                     original=original,
                     parsed=parsed,
                     posted_on=datetime,
@@ -123,7 +122,6 @@ class Command(BaseCommand):
                         thread=thread,
                         poster=user,
                         poster_name=user.username,
-                        poster_ip=fake.ipv4(),
                         original=original,
                         parsed=parsed,
                         is_unapproved=is_unapproved,

+ 31 - 0
misago/legal/admin.py

@@ -0,0 +1,31 @@
+from django.conf.urls import url
+from django.utils.translation import ugettext_lazy as _
+
+from .views.admin import (
+    AgreementsList, DeleteAgreement, EditAgreement, NewAgreement, SetAgreementAsActive
+)
+
+
+class MisagoAdminExtension(object):
+    def register_urlpatterns(self, urlpatterns):
+        # Legal Agreements
+        urlpatterns.namespace(r'^agreements/', 'agreements', 'users')
+        urlpatterns.patterns(
+            'users:agreements',
+            url(r'^$', AgreementsList.as_view(), name='index'),
+            url(r'^(?P<page>\d+)/$', AgreementsList.as_view(), name='index'),
+            url(r'^new/$', NewAgreement.as_view(), name='new'),
+            url(r'^edit/(?P<pk>\d+)/$', EditAgreement.as_view(), name='edit'),
+            url(r'^delete/(?P<pk>\d+)/$', DeleteAgreement.as_view(), name='delete'),
+            url(r'^set-as-active/(?P<pk>\d+)/$', SetAgreementAsActive.as_view(), name='set-as-active'),
+        )
+        
+    def register_navigation_nodes(self, site):
+        site.add_node(
+            name=_("Agreements"),
+            icon='fa fa-check-square-o',
+            parent='misago:admin:users',
+            after='misago:admin:users:data-downloads:index',
+            namespace='misago:admin:users:agreements',
+            link='misago:admin:users:agreements:index',
+        )

+ 29 - 0
misago/legal/api.py

@@ -0,0 +1,29 @@
+from django.contrib.auth import logout
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
+from django.utils.translation import ugettext as _
+
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+
+from .models import Agreement
+from .utils import save_user_agreement_acceptance
+
+
+@api_view(['POST'])
+def submit_agreement(request, pk):
+    agreement = get_object_or_404(Agreement, is_active=True, pk=pk)
+
+    if agreement.id in request.user.agreements:
+        raise PermissionDenied(_("You have already accepted this agreement."))
+
+    if request.data.get('accept') is True:
+        save_user_agreement_acceptance(request.user, agreement, commit=True)
+    elif request.data.get('accept') is False:
+        if not request.user.is_staff:
+            request.user.mark_for_delete()
+            logout(request)
+    else:
+        raise PermissionDenied(_("You need to submit a valid choice."))
+
+    return Response({'detail': 'ok'})

+ 3 - 0
misago/legal/apps.py

@@ -5,3 +5,6 @@ class MisagoLegalConfig(AppConfig):
     name = 'misago.legal'
     label = 'misago_legal'
     verbose_name = "Misago Legal"
+
+    def ready(self):
+        from . import signals as _

+ 44 - 22
misago/legal/context_processors.py

@@ -1,28 +1,50 @@
 from django.urls import reverse
 
-from misago.conf import settings
+from .models import Agreement
+from .utils import get_parsed_agreement_text, get_required_user_agreement
 
 
+# fixme: rename this context processor to more suitable name
 def legal_links(request):
-    if settings.privacy_policy_link:
-        request.frontend_context['url'].update({
-            'privacy_policy': settings.privacy_policy_link,
-        })
-    elif settings.privacy_policy:
-        request.frontend_context['url'].update({
-            'privacy_policy': reverse('misago:privacy-policy'),
-        })
-
-    if settings.terms_of_service_link:
-        request.frontend_context['url'].update({
-            'tos': settings.terms_of_service_link,
-        })
-    elif settings.terms_of_service:
-        request.frontend_context['url'].update({
-            'tos': reverse('misago:terms-of-service'),
-        })
-
-    return {
-        'privacy_policy': settings.privacy_policy_link or settings.privacy_policy,
-        'terms_of_service': settings.terms_of_service_link or settings.terms_of_service,
+    agreements = Agreement.objects.get_agreements()
+
+    legal_context = {
+        'TERMS_OF_SERVICE_ID': None,
+        'TERMS_OF_SERVICE_URL': None,
+        'PRIVACY_POLICY_ID': None,
+        'PRIVACY_POLICY_URL': None,
+        'misago_agreement': None,
     }
+
+    terms_of_service = agreements.get(Agreement.TYPE_TOS)
+    if terms_of_service:
+        legal_context['TERMS_OF_SERVICE_ID'] = terms_of_service['id']
+        if terms_of_service['link']:
+            legal_context['TERMS_OF_SERVICE_URL'] = terms_of_service['link']
+        elif terms_of_service['text']:
+            legal_context['TERMS_OF_SERVICE_URL'] = reverse('misago:terms-of-service')
+
+    privacy_policy = agreements.get(Agreement.TYPE_PRIVACY)
+    if privacy_policy:
+        legal_context['PRIVACY_POLICY_ID'] = privacy_policy['id']
+        if privacy_policy['link']:
+            legal_context['PRIVACY_POLICY_URL'] = privacy_policy['link']
+        elif privacy_policy['text']:
+            legal_context['PRIVACY_POLICY_URL'] = reverse('misago:privacy-policy')
+
+    if legal_context:
+        request.frontend_context.update(legal_context)
+
+    required_agreement = get_required_user_agreement(request.user, agreements)
+    if required_agreement:
+        request.frontend_context['REQUIRED_AGREEMENT_API'] = reverse(
+            'misago:api:submit-agreement', kwargs={'pk': required_agreement.pk})
+
+        legal_context['misago_agreement'] = {
+            'type': required_agreement.get_type_display(),
+            'title': required_agreement.get_final_title(),
+            'link': required_agreement.link,
+            'text': get_parsed_agreement_text(request, required_agreement)
+        }
+
+    return legal_context

+ 79 - 0
misago/legal/forms.py

@@ -0,0 +1,79 @@
+from django import forms
+from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
+
+from .models import Agreement
+from .utils import set_agreement_as_active
+
+
+class AgreementForm(forms.ModelForm):
+    type = forms.ChoiceField(label=_("Type"), choices=Agreement.TYPE_CHOICES)
+    title = forms.CharField(
+        label=_("Title"),
+        help_text=_("Optional, leave empty for agreement to be named after its type."),
+        required=False,
+    )
+    is_active = forms.BooleanField(
+        label=_("Set as active for its type"),
+        help_text=_(
+            "If other agreement is already active for this type, it will be unset and replaced "
+            "with this one. "
+            "Misago will ask users who didn't accept this agreement to do so before allowing them "
+            "to continue using the site's features."
+        ),
+        required=False,
+    )
+    link = forms.URLField(
+        label=_("Link"),
+        help_text=_("If your agreement is located on other page, enter here a link to it."),
+        required=False,
+    )
+    text = forms.CharField(
+        label=_("Text"),
+        help_text=_("You can use Markdown syntax for rich text elements."),
+        widget=forms.Textarea,
+        required=False,
+    )
+
+    class Meta:
+        model = Agreement
+        fields = ['type', 'title', 'link', 'text', 'is_active']
+
+    def clean(self):
+        data = super(AgreementForm, self).clean()
+
+        if not data.get('link') and not data.get('text'):
+            raise forms.ValidationError(_("Please fill in agreement link or text."))
+
+        return data
+
+    def save(self):
+        agreement = super(AgreementForm, self).save()
+        if agreement.is_active:
+            set_agreement_as_active(agreement)
+        Agreement.objects.invalidate_cache()
+        return agreement
+
+
+class SearchAgreementsForm(forms.Form):
+    type = forms.MultipleChoiceField(
+        label=_("Type"),
+        required=False,
+        choices=Agreement.TYPE_CHOICES,
+    )
+    content = forms.CharField(
+        label=_("Content"),
+        required=False,
+    )
+
+    def filter_queryset(self, search_criteria, queryset):
+        criteria = search_criteria
+        if criteria.get('type') is not None:
+            queryset = queryset.filter(type__in=criteria['type'])
+
+        if criteria.get('content'):
+            search_title = Q(title__icontains=criteria['content'])
+            search_text = Q(text__icontains=criteria['content'])
+            queryset = queryset.filter(search_title | search_text)
+
+        return queryset

+ 50 - 0
misago/legal/migrations/0002_agreement_useragreement.py

@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.15 on 2018-08-15 20:58
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('misago_legal', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Agreement',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('type', models.CharField(choices=[('terms_of_service', 'Terms of service'), ('privacy_policy', 'Privacy policy')], db_index=True, default='terms_of_service', max_length=20)),
+                ('title', models.CharField(blank=True, max_length=255, null=True)),
+                ('link', models.URLField(blank=True, max_length=255, null=True)),
+                ('text', models.TextField(blank=True, null=True)),
+                ('is_active', models.BooleanField(default=False)),
+                ('created_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('created_by_name', models.CharField(blank=True, max_length=255, null=True)),
+                ('last_modified_on', models.DateTimeField(blank=True, null=True)),
+                ('last_modified_by_name', models.CharField(blank=True, max_length=255, null=True)),
+                ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('last_modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='UserAgreement',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('accepted_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('agreement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accepted_by', to='misago_legal.Agreement')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-pk'],
+            },
+        ),
+    ]

+ 84 - 0
misago/legal/migrations/0003_create_agreements_from_settings.py

@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.15 on 2018-08-16 14:22
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+from misago.conf.migrationutils import migrate_settings_group
+from misago.legal.models import Agreement as MisagoAgreement
+
+
+_ = lambda s: s
+
+
+LEGAL_SETTINGS = [
+    'terms_of_service_title',
+    'terms_of_service_link',
+    'terms_of_service',
+    'privacy_policy_title',
+    'privacy_policy_link',
+    'privacy_policy',
+]
+
+
+def create_legal_settings_group(apps, schema_editor):
+    Agreement = apps.get_model('misago_legal', 'Agreement')
+    Setting = apps.get_model('misago_conf', 'Setting')
+    
+    legal_conf = {}
+    for setting in Setting.objects.filter(setting__in=LEGAL_SETTINGS):
+        legal_conf[setting.setting] = setting.dry_value
+
+    if legal_conf['terms_of_service'] or legal_conf['terms_of_service_link']:
+        Agreement.objects.create(
+            type=MisagoAgreement.TYPE_TOS,
+            title=legal_conf['terms_of_service_title'],
+            link=legal_conf['terms_of_service_link'],
+            text=legal_conf['terms_of_service'],
+            is_active=True,
+        )
+
+    if legal_conf['privacy_policy'] or legal_conf['privacy_policy_link']:
+        Agreement.objects.create(
+            type=MisagoAgreement.TYPE_PRIVACY,
+            title=legal_conf['privacy_policy_title'],
+            link=legal_conf['privacy_policy_link'],
+            text=legal_conf['privacy_policy'],
+            is_active=True,
+        )
+
+    MisagoAgreement.objects.invalidate_cache()
+
+
+def delete_deprecated_settings(apps, schema_editor):
+    migrate_settings_group(
+        apps, {
+            'key': 'legal',
+            'name': _("Legal information"),
+            'description': _("Those settings allow you to set additional legal information for your forum."),
+            'settings': [
+                {
+                    'setting': 'forum_footnote',
+                    'name': _("Footnote"),
+                    'description': _("Short message displayed in forum footer."),
+                    'legend': _("Forum footer"),
+                    'field_extra': {
+                        'max_length': 300,
+                    },
+                    'is_public': True,
+                },
+            ],
+        }
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_legal', '0002_agreement_useragreement'),
+    ]
+
+    operations = [
+        migrations.RunPython(create_legal_settings_group),
+        migrations.RunPython(delete_deprecated_settings),
+    ]

+ 95 - 1
misago/legal/models.py

@@ -1 +1,95 @@
-# Empty models file that triggers migrations for this app
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from misago.conf import settings
+from misago.core.cache import cache
+
+
+CACHE_KEY = 'misago_agreements'
+
+
+class AgreementManager(models.Manager):
+    def invalidate_cache(self):
+        cache.delete(CACHE_KEY)
+
+    def get_agreements(self):
+        agreements = self.get_agreements_from_cache()
+        if agreements == 'nada':
+            agreements = self.get_agreements_from_db()
+            cache.set(CACHE_KEY, agreements)
+        return agreements
+
+    def get_agreements_from_cache(self):
+        return cache.get(CACHE_KEY, 'nada')
+
+    def get_agreements_from_db(self):
+        agreements = {}
+        for agreement in Agreement.objects.filter(is_active=True):
+            agreements[agreement.type] = {
+                'id': agreement.id,
+                'title': agreement.get_final_title(),
+                'link': agreement.link,
+                'text': bool(agreement.text),
+            }
+        return agreements
+
+
+class Agreement(models.Model):
+    TYPE_TOS = 'terms_of_service'
+    TYPE_PRIVACY = 'privacy_policy'
+    TYPE_CHOICES = [
+        (TYPE_TOS, _('Terms of service')),
+        (TYPE_PRIVACY, _('Privacy policy')),
+    ]
+
+    type = models.CharField(
+        max_length=20,
+        default=TYPE_TOS,
+        choices=TYPE_CHOICES,
+        db_index=True,
+    )
+    title = models.CharField(max_length=255, null=True, blank=True)
+    link = models.URLField(max_length=255, null=True, blank=True)
+    text = models.TextField(null=True, blank=True)
+    is_active = models.BooleanField(default=False)
+    created_on = models.DateTimeField(default=timezone.now)
+    created_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.SET_NULL,
+        blank=True,
+        null=True,
+        related_name='+',
+    )
+    created_by_name = models.CharField(max_length=255, null=True, blank=True)
+    last_modified_on = models.DateTimeField(null=True, blank=True)
+    last_modified_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.SET_NULL,
+        blank=True,
+        null=True,
+        related_name='+',
+    )
+    last_modified_by_name = models.CharField(max_length=255, null=True, blank=True)
+
+    objects = AgreementManager()
+
+    def get_final_title(self):
+        return self.title or self.get_type_display()
+
+    def set_created_by(self, user):
+        self.created_by = user
+        self.created_by_name = user.username
+
+    def set_last_modified_by(self, user):
+        self.last_modified_by = user
+        self.last_modified_by_name = user.username
+
+
+class UserAgreement(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
+    agreement = models.ForeignKey(Agreement, related_name='accepted_by')
+    accepted_on = models.DateTimeField(default=timezone.now)
+
+    class Meta:
+        ordering = ["-pk"]

+ 16 - 0
misago/legal/signals.py

@@ -0,0 +1,16 @@
+from django.dispatch import receiver
+
+from misago.users.signals import anonymize_user_data, username_changed
+
+from .models import Agreement
+
+
+@receiver([anonymize_user_data, username_changed])
+def update_usernames(sender, **kwargs):
+    Agreement.objects.filter(created_by=sender).update(
+        created_by_name=sender.username,
+    )
+
+    Agreement.objects.filter(last_modified_by=sender).update(
+        last_modified_by_name=sender.username,
+    )

+ 0 - 193
misago/legal/tests.py

@@ -1,193 +0,0 @@
-from django.test import TestCase
-from django.urls import reverse
-
-from misago.conf import settings
-
-from .context_processors import legal_links
-
-
-class MockRequest(object):
-    def __init__(self):
-        self.frontend_context = {'url': {}}
-
-
-class PrivacyPolicyTests(TestCase):
-    def tearDown(self):
-        settings.reset_settings()
-
-    def test_404_on_no_policy(self):
-        """policy view returns 404 when no policy is set"""
-        self.assertFalse(settings.privacy_policy_link)
-        self.assertFalse(settings.privacy_policy)
-
-        response = self.client.get(reverse('misago:privacy-policy'))
-        self.assertEqual(response.status_code, 404)
-
-    def test_301_on_link_policy(self):
-        """policy view returns 302 redirect when link is set"""
-        settings.override_setting('privacy_policy_link', 'http://test.com')
-        settings.override_setting('privacy_policy', 'Lorem ipsum')
-        self.assertTrue(settings.privacy_policy_link)
-        self.assertTrue(settings.privacy_policy)
-
-        response = self.client.get(reverse('misago:privacy-policy'))
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], 'http://test.com')
-
-    def test_200_on_link_policy(self):
-        """policy view returns 200 when custom tos content is set"""
-        settings.override_setting('privacy_policy_title', 'Test Policy')
-        settings.override_setting('privacy_policy', 'Lorem ipsum dolor')
-        self.assertTrue(settings.privacy_policy_title)
-        self.assertTrue(settings.privacy_policy)
-
-        response = self.client.get(reverse('misago:privacy-policy'))
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, 'Test Policy')
-        self.assertContains(response, 'Lorem ipsum dolor')
-
-    def test_context_processor_no_policy(self):
-        """context processor has no privacy policy link"""
-        request = MockRequest()
-
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': None,
-            'terms_of_service': None,
-        })
-        self.assertEqual(request.frontend_context['url'], {})
-
-    def test_context_processor_misago_policy(self):
-        """context processor has TOS link to Misago view"""
-        request = MockRequest()
-
-        settings.override_setting('privacy_policy', 'Lorem ipsum')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': 'Lorem ipsum',
-            'terms_of_service': None,
-        })
-        self.assertEqual(request.frontend_context['url'], {
-            'privacy_policy': reverse('misago:privacy-policy'),
-        })
-
-    def test_context_processor_remote_policy(self):
-        """context processor has TOS link to remote url"""
-        request = MockRequest()
-
-        settings.override_setting('privacy_policy_link', 'http://test.com')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': 'http://test.com',
-            'terms_of_service': None,
-        })
-        self.assertEqual(request.frontend_context['url'], {
-            'privacy_policy': 'http://test.com',
-        })
-
-        # set misago view too
-        settings.override_setting('privacy_policy', 'Lorem ipsum')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': 'http://test.com',
-            'terms_of_service': None,
-        })
-        self.assertEqual(request.frontend_context['url'], {
-            'privacy_policy': 'http://test.com',
-        })
-
-
-class TermsOfServiceTests(TestCase):
-    def tearDown(self):
-        settings.reset_settings()
-
-    def test_404_on_no_tos(self):
-        """TOS view returns 404 when no TOS is set"""
-        self.assertFalse(settings.terms_of_service_link)
-        self.assertFalse(settings.terms_of_service)
-
-        response = self.client.get(reverse('misago:terms-of-service'))
-        self.assertEqual(response.status_code, 404)
-
-    def test_301_on_link_tos(self):
-        """TOS view returns 302 redirect when link is set"""
-        settings.override_setting('terms_of_service_link', 'http://test.com')
-        settings.override_setting('terms_of_service', 'Lorem ipsum')
-        self.assertTrue(settings.terms_of_service_link)
-        self.assertTrue(settings.terms_of_service)
-
-        response = self.client.get(reverse('misago:terms-of-service'))
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], 'http://test.com')
-
-    def test_200_on_link_tos(self):
-        """TOS view returns 200 when custom tos content is set"""
-        settings.override_setting('terms_of_service_title', 'Test ToS')
-        settings.override_setting('terms_of_service', 'Lorem ipsum dolor')
-        self.assertTrue(settings.terms_of_service_title)
-        self.assertTrue(settings.terms_of_service)
-
-        response = self.client.get(reverse('misago:terms-of-service'))
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, 'Test ToS')
-        self.assertContains(response, 'Lorem ipsum dolor')
-
-    def test_context_processor_no_tos(self):
-        """context processor has no TOS link"""
-        request = MockRequest()
-
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': None,
-            'terms_of_service': None,
-        })
-        self.assertEqual(request.frontend_context['url'], {})
-
-    def test_context_processor_misago_tos(self):
-        """context processor has TOS link to Misago view"""
-        request = MockRequest()
-
-        settings.override_setting('terms_of_service', 'Lorem ipsum')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': None,
-            'terms_of_service': 'Lorem ipsum',
-        })
-        self.assertEqual(
-            request.frontend_context['url'], {
-                'tos': reverse('misago:terms-of-service'),
-            }
-        )
-
-    def test_context_processor_remote_tos(self):
-        """context processor has TOS link to remote url"""
-        request = MockRequest()
-
-        settings.override_setting('terms_of_service_link', 'http://test.com')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': None,
-            'terms_of_service': 'http://test.com',
-        })
-        self.assertEqual(request.frontend_context['url'], {
-            'tos': 'http://test.com',
-        })
-
-        # set misago view too
-        settings.override_setting('terms_of_service', 'Lorem ipsum')
-        context_dict = legal_links(request)
-
-        self.assertEqual(context_dict, {
-            'privacy_policy': None,
-            'terms_of_service': 'http://test.com',
-        })
-        self.assertEqual(request.frontend_context['url'], {
-            'tos': 'http://test.com',
-        })

+ 0 - 0
misago/api/tests/__init__.py → misago/legal/tests/__init__.py


+ 313 - 0
misago/legal/tests/test_admin_views.py

@@ -0,0 +1,313 @@
+from django.urls import reverse
+
+from misago.admin.testutils import AdminTestCase
+from misago.legal.models import Agreement
+
+
+class AgreementAdminViewsTests(AdminTestCase):
+    def test_link_registered(self):
+        """admin nav contains agreements link"""
+        response = self.client.get(reverse('misago:admin:users:accounts:index'))
+
+        response = self.client.get(response['location'])
+        self.assertContains(response, reverse('misago:admin:users:agreements:index'))
+
+    def test_list_view(self):
+        """agreements list view returns 200"""
+        response = self.client.get(reverse('misago:admin:users:agreements:index'))
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(response['location'])
+        self.assertEqual(response.status_code, 200)
+
+    def test_mass_delete(self):
+        """adminview deletes multiple agreements"""
+        for i in range(10):
+            response = self.client.post(
+                reverse('misago:admin:users:agreements:new'),
+                data={
+                    'type': Agreement.TYPE_TOS,
+                    'text': 'test agreement!',
+                },
+            )
+            self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(Agreement.objects.count(), 10)
+
+        agreements_pks = []
+        for agreement in Agreement.objects.iterator():
+            agreements_pks.append(agreement.pk)
+
+        response = self.client.post(
+            reverse('misago:admin:users:agreements:index'),
+            data={
+                'action': 'delete',
+                'selected_items': agreements_pks,
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(Agreement.objects.count(), 0)
+
+    def test_new_view(self):
+        """new agreement view has no showstoppers"""
+        response = self.client.get(reverse('misago:admin:users:agreements:new'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Test Rules',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('misago:admin:users:agreements:index'))
+        response = self.client.get(response['location'])
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Test Rules')
+
+        test_agreement = Agreement.objects.get(type=Agreement.TYPE_TOS)
+        self.assertIsNone(test_agreement.last_modified_on)
+        self.assertIsNone(test_agreement.last_modified_by)
+        self.assertIsNone(test_agreement.last_modified_by_name)
+
+    def test_new_view_change_active(self):
+        """new agreement view creates new active agreement"""
+        response = self.client.get(reverse('misago:admin:users:agreements:new'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Old Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+                'is_active': True,
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'New Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+                'is_active': True,
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        test_agreement = Agreement.objects.get(is_active=True)
+        self.assertEqual(test_agreement.title, 'New Active')
+
+    def test_edit_view(self):
+        """edit agreement view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Test Rules',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+
+        test_agreement = Agreement.objects.get(type=Agreement.TYPE_TOS)
+        form_link = reverse(
+            'misago:admin:users:agreements:edit',
+            kwargs={
+                'pk': test_agreement.pk,
+            },
+        )
+
+        response = self.client.post(
+            form_link,
+            data={
+                'type': Agreement.TYPE_PRIVACY,
+                'title': 'Test Privacy',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('misago:admin:users:agreements:index'))
+        response = self.client.get(response['location'])
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Test Privacy')
+
+        updated_agreement = Agreement.objects.get(type=Agreement.TYPE_PRIVACY)
+        self.assertTrue(updated_agreement.last_modified_on)
+        self.assertEqual(updated_agreement.last_modified_by, self.user)
+        self.assertEqual(updated_agreement.last_modified_by_name, self.user.username)
+
+    def test_edit_view_change_active(self):
+        """edit agreement view sets new active"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Old Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+                'is_active': True
+            },
+        )
+
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'New Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+
+        test_agreement = Agreement.objects.get(title='New Active')
+        form_link = reverse(
+            'misago:admin:users:agreements:edit',
+            kwargs={
+                'pk': test_agreement.pk,
+            },
+        )
+
+        response = self.client.post(
+            form_link,
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Updated Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+                'is_active': True
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        updated_agreement = Agreement.objects.get(is_active=True)
+        self.assertEqual(updated_agreement.title, 'Updated Active')
+
+    def test_delete_view(self):
+        """delete agreement view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Test Rules',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+
+        test_agreement = Agreement.objects.get(type=Agreement.TYPE_TOS)
+
+        response = self.client.post(
+            reverse(
+                'misago:admin:users:agreements:delete',
+                kwargs={
+                    'pk': test_agreement.pk,
+                },
+            )
+        )
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('misago:admin:users:agreements:index'))
+        self.client.get(response['location'])
+        response = self.client.get(response['location'])
+
+        self.assertEqual(response.status_code, 200)
+        self.assertNotContains(response, test_agreement.title)
+
+    def test_set_as_active_view(self):
+        """set agreement as active view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Test Rules',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+
+        test_agreement = Agreement.objects.get(type=Agreement.TYPE_TOS)
+
+        response = self.client.post(
+            reverse(
+                'misago:admin:users:agreements:set-as-active',
+                kwargs={
+                    'pk': test_agreement.pk,
+                },
+            )
+        )
+        self.assertEqual(response.status_code, 302)
+
+        updated_agreement = Agreement.objects.get(is_active=True)
+        self.assertEqual(updated_agreement, test_agreement)
+
+    def test_set_as_active_view_change_active(self):
+        """set agreement as active view changes current active"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'Old Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+                'is_active': True,
+            },
+        )
+
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'title': 'New Active',
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'link': 'https://example.com/rules/',
+            },
+        )
+
+        test_agreement = Agreement.objects.get(title='New Active')
+
+        response = self.client.post(
+            reverse(
+                'misago:admin:users:agreements:set-as-active',
+                kwargs={
+                    'pk': test_agreement.pk,
+                },
+            )
+        )
+        self.assertEqual(response.status_code, 302)
+
+        updated_agreement = Agreement.objects.get(is_active=True)
+        self.assertEqual(updated_agreement, test_agreement)
+
+    def test_is_active_type_separation(self):
+        """is_active flag is per type"""
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_TOS,
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'is_active': True,
+            },
+        )
+
+        self.client.post(
+            reverse('misago:admin:users:agreements:new'),
+            data={
+                'type': Agreement.TYPE_PRIVACY,
+                'text': 'Lorem ipsum dolor met sit amet elit',
+                'is_active': True,
+            },
+        )
+
+        active_count = Agreement.objects.filter(is_active=True).count()
+        self.assertEqual(active_count, 2)

+ 105 - 0
misago/legal/tests/test_api.py

@@ -0,0 +1,105 @@
+import json
+
+from django.urls import reverse
+
+from misago.legal.models import Agreement
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class SubmitAgreementTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(SubmitAgreementTests, self).setUp()
+
+        self.agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        self.api_link = reverse(
+            'misago:api:submit-agreement', kwargs={'pk': self.agreement.pk})
+
+    def post_json(self, data):
+        return self.client.post(
+            self.api_link, json.dumps(data), content_type='application/json')
+
+    def test_anonymous(self):
+        self.logout_user()
+
+        response = self.client.post(self.api_link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This action is not available to guests.",
+        })
+
+    def test_get_request(self):
+        response = self.client.get(self.api_link)
+        self.assertEqual(response.status_code, 405)
+        self.assertEqual(response.json(), {
+            'detail': 'Method "GET" not allowed.',
+        })
+
+    def test_invalid_agreement_id(self):
+        api_link = reverse(
+            'misago:api:submit-agreement', kwargs={'pk': self.agreement.pk + 1})
+
+        response = self.client.post(api_link)
+        self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "Not found.",
+        })
+
+    def test_agreement_already_accepted(self):
+        self.user.agreements.append(self.agreement.id)
+        self.user.save()
+
+        response = self.client.post(self.api_link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have already accepted this agreement.",
+        })
+
+    def test_no_accept_sent(self):
+        response = self.client.post(self.api_link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You need to submit a valid choice.",
+        })
+
+    def test_invalid_accept_sent(self):
+        response = self.post_json({'accept': 1})
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You need to submit a valid choice.",
+        })
+
+    def test_accept_false(self):
+        response = self.post_json({'accept': False})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {'detail': 'ok'})
+
+        self.user.refresh_from_db()
+        self.assertTrue(self.user.is_deleting_account)
+        self.assertFalse(self.user.is_active)
+
+    def test_accept_false_staff(self):
+        self.user.is_staff = True
+        self.user.save()
+
+        response = self.post_json({'accept': False})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {'detail': 'ok'})
+
+        self.user.refresh_from_db()
+        self.assertFalse(self.user.is_deleting_account)
+        self.assertTrue(self.user.is_active)
+
+    def test_accept_true(self):
+        response = self.post_json({'accept': True})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {'detail': 'ok'})
+
+        self.user.refresh_from_db()
+        self.assertEqual(self.user.agreements, [self.agreement.id])
+        self.assertFalse(self.user.is_deleting_account)
+        self.assertTrue(self.user.is_active)

+ 186 - 0
misago/legal/tests/test_context_processors.py

@@ -0,0 +1,186 @@
+from django.urls import reverse
+
+from misago.legal.context_processors import legal_links
+from misago.legal.models import Agreement
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class MockRequest(object):
+    def __init__(self, user):
+        self.user = user
+        self.frontend_context = {}
+
+    def get_host(self):
+        return 'testhost.com'
+
+
+class PrivacyPolicyTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PrivacyPolicyTests, self).setUp()
+
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_context_processor_no_policy(self):
+        """context processor has no TOS link"""
+        context_dict = legal_links(MockRequest(self.user))
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': None,
+            'TERMS_OF_SERVICE_URL': None,
+            'PRIVACY_POLICY_ID': None,
+            'PRIVACY_POLICY_URL': None,
+            'misago_agreement': None,
+        })
+
+    def test_context_processor_misago_policy(self):
+        """context processor has TOS link to Misago view"""
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': None,
+            'TERMS_OF_SERVICE_URL': None,
+            'PRIVACY_POLICY_ID': agreement.id,
+            'PRIVACY_POLICY_URL': reverse('misago:privacy-policy'),
+            'misago_agreement': {
+                'type': 'Privacy policy',
+                'title': 'Privacy policy',
+                'link': None,
+                'text': '<p>Lorem ipsum</p>',
+            },
+        })
+
+    def test_context_processor_remote_policy(self):
+        """context processor has TOS link to remote url"""
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='http://test.com',
+            is_active=True,
+        )
+
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': None,
+            'TERMS_OF_SERVICE_URL': None,
+            'PRIVACY_POLICY_ID': agreement.id,
+            'PRIVACY_POLICY_URL': 'http://test.com',
+            'misago_agreement': {
+                'type': 'Privacy policy',
+                'title': 'Privacy policy',
+                'link': 'http://test.com',
+                'text': None,
+            },
+        })
+
+        # set misago view too
+        agreement.text = 'Lorem ipsum'
+        agreement.save()
+        
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': None,
+            'TERMS_OF_SERVICE_URL': None,
+            'PRIVACY_POLICY_ID': agreement.id,
+            'PRIVACY_POLICY_URL': 'http://test.com',
+            'misago_agreement': {
+                'type': 'Privacy policy',
+                'title': 'Privacy policy',
+                'link': 'http://test.com',
+                'text': '<p>Lorem ipsum</p>',
+            },
+        })
+
+
+class TermsOfServiceTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(TermsOfServiceTests, self).setUp()
+        
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_context_processor_no_tos(self):
+        """context processor has no TOS link"""
+        context_dict = legal_links(MockRequest(self.user))
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': None,
+            'TERMS_OF_SERVICE_URL': None,
+            'PRIVACY_POLICY_ID': None,
+            'PRIVACY_POLICY_URL': None,
+            'misago_agreement': None,
+        })
+
+    def test_context_processor_misago_tos(self):
+        """context processor has TOS link to Misago view"""
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': agreement.id,
+            'TERMS_OF_SERVICE_URL': reverse('misago:terms-of-service'),
+            'PRIVACY_POLICY_ID': None,
+            'PRIVACY_POLICY_URL': None,
+            'misago_agreement': {
+                'type': 'Terms of service',
+                'title': 'Terms of service',
+                'link': None,
+                'text': '<p>Lorem ipsum</p>',
+            }
+        })
+
+    def test_context_processor_remote_tos(self):
+        """context processor has TOS link to remote url"""
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='http://test.com',
+            is_active=True,
+        )
+
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': agreement.id,
+            'TERMS_OF_SERVICE_URL': 'http://test.com',
+            'PRIVACY_POLICY_ID': None,
+            'PRIVACY_POLICY_URL': None,
+            'misago_agreement': {
+                'type': 'Terms of service',
+                'title': 'Terms of service',
+                'link': 'http://test.com',
+                'text': None,
+            }
+        })
+
+        # set misago view too
+        agreement.text = 'Lorem ipsum'
+        agreement.save()
+
+        context_dict = legal_links(MockRequest(self.user))
+
+        self.assertEqual(context_dict, {
+            'TERMS_OF_SERVICE_ID': agreement.id,
+            'TERMS_OF_SERVICE_URL': 'http://test.com',
+            'PRIVACY_POLICY_ID': None,
+            'PRIVACY_POLICY_URL': None,
+            'misago_agreement': {
+                'type': 'Terms of service',
+                'title': 'Terms of service',
+                'link': 'http://test.com',
+                'text': '<p>Lorem ipsum</p>',
+            },
+        })

+ 96 - 0
misago/legal/tests/test_required_agreement.py

@@ -0,0 +1,96 @@
+from django.urls import reverse
+
+from misago.legal.models import Agreement
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class RequiredAgreementTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(RequiredAgreementTests, self).setUp()
+
+        self.test_link = reverse('misago:index')
+
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_tos_link(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='https://test-agreement.com',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_tos_text(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_tos_text_and_link(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='https://test-agreement.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_privacy_link(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://test-agreement.com',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_privacy_text(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_privacy_text_and_link(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://test-agreement.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_both(self):
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='https://test-agreement.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://test-agreement.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(self.test_link)
+        self.assertEqual(response.status_code, 200)

+ 186 - 0
misago/legal/tests/test_utils.py

@@ -0,0 +1,186 @@
+from django.test import TestCase
+
+from misago.legal.models import Agreement, UserAgreement
+from misago.legal.utils import (
+    get_parsed_agreement_text, get_required_user_agreement, save_user_agreement_acceptance,
+    set_agreement_as_active
+)
+from misago.users.testutils import UserTestCase
+
+
+class MockRequest(object):
+    def __init__(self, user=None):
+        self.user = user
+        self.frontend_context = {}
+
+    def get_host(self):
+        return 'testhost.com'
+
+
+class GetParsedAgreementTextTests(TestCase):
+    def test_agreement_no_text(self):
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            is_active=True,
+        )
+
+        result = get_parsed_agreement_text(MockRequest(), agreement)
+        self.assertIsNone(result)
+
+    def test_agreement_link_and_text(self):
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        result = get_parsed_agreement_text(MockRequest(), agreement)
+        self.assertEqual(result, '<p>Lorem ipsum</p>')
+
+    def test_agreement_text(self):
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        result = get_parsed_agreement_text(MockRequest(), agreement)
+        self.assertEqual(result, '<p>Lorem ipsum</p>')
+
+
+class GetRequiredUserAgreementTests(UserTestCase):
+    def setUp(self):
+        Agreement.objects.invalidate_cache()
+
+        self.agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        self.agreements = Agreement.objects.get_agreements()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_anonymous_user(self):
+        anonymous_user = self.get_anonymous_user()
+        result = get_required_user_agreement(anonymous_user, self.agreements)
+        self.assertIsNone(result)
+
+    def test_authenticated_user_no_agreements(self):
+        authenticated_user = self.get_authenticated_user()
+        result = get_required_user_agreement(authenticated_user, {})
+        self.assertIsNone(result)
+
+    def test_authenticated_user(self):
+        authenticated_user = self.get_authenticated_user()
+        result = get_required_user_agreement(authenticated_user, self.agreements)
+        self.assertEqual(result, self.agreement)
+
+    def test_authenticated_user_with_agreement(self):
+        authenticated_user = self.get_authenticated_user()
+        authenticated_user.agreements.append(self.agreement.pk)
+
+        result = get_required_user_agreement(authenticated_user, self.agreements)
+        self.assertIsNone(result)
+
+
+class SaveUserAgreementAcceptance(UserTestCase):
+    def test_no_commit(self):
+        user = self.get_authenticated_user()
+
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+        )
+
+        save_user_agreement_acceptance(user, agreement)
+        self.assertEqual(user.agreements, [agreement.id])
+
+        user.refresh_from_db()
+        self.assertEqual(user.agreements, [])
+
+        UserAgreement.objects.get(user=user, agreement=agreement)
+        self.assertEqual(UserAgreement.objects.count(), 1)
+
+    def test_commit(self):
+        user = self.get_authenticated_user()
+
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+        )
+
+        save_user_agreement_acceptance(user, agreement, commit=True)
+        self.assertEqual(user.agreements, [agreement.id])
+
+        user.refresh_from_db()
+        self.assertEqual(user.agreements, [agreement.id])
+
+        UserAgreement.objects.get(user=user, agreement=agreement)
+        self.assertEqual(UserAgreement.objects.count(), 1)
+
+
+class SetAgreementAsActiveTests(TestCase):
+     def test_inactive_agreement(self):
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+        )
+
+        set_agreement_as_active(agreement)
+        self.assertTrue(agreement.is_active)
+
+        agreement.refresh_from_db()
+        self.assertFalse(agreement.is_active)
+
+     def test_inactive_agreement_commit(self):
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+        )
+
+        set_agreement_as_active(agreement, commit=True)
+        self.assertTrue(agreement.is_active)
+
+        agreement.refresh_from_db()
+        self.assertTrue(agreement.is_active)
+        
+     def test_change_active_agreement(self):
+        old_agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        new_agreement = Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+        )
+
+        other_type_agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        set_agreement_as_active(new_agreement, commit=True)
+
+        old_agreement.refresh_from_db()
+        new_agreement.refresh_from_db()
+        other_type_agreement.refresh_from_db()
+
+        self.assertFalse(old_agreement.is_active)
+        self.assertTrue(new_agreement.is_active)
+        self.assertTrue(other_type_agreement.is_active)

+ 84 - 0
misago/legal/tests/test_views.py

@@ -0,0 +1,84 @@
+from django.test import TestCase
+from django.urls import reverse
+
+from misago.legal.models import Agreement
+
+
+class PrivacyPolicyTests(TestCase):
+    def setUp(self):
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_404_on_no_policy(self):
+        """policy view returns 404 when no policy is set"""
+        response = self.client.get(reverse('misago:privacy-policy'))
+        self.assertEqual(response.status_code, 404)
+
+    def test_301_on_link_policy(self):
+        """policy view returns 302 redirect when link is set"""
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            link='http://test.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(reverse('misago:privacy-policy'))
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], 'http://test.com')
+
+    def test_200_on_link_policy(self):
+        """policy view returns 200 when custom tos content is set"""
+        Agreement.objects.create(
+            type=Agreement.TYPE_PRIVACY,
+            title='Test Policy',
+            text='Lorem ipsum dolor',
+            is_active=True,
+        )
+
+        response = self.client.get(reverse('misago:privacy-policy'))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Test Policy')
+        self.assertContains(response, 'Lorem ipsum dolor')
+
+
+class TermsOfServiceTests(TestCase):
+    def setUp(self):
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
+    def test_404_on_no_tos(self):
+        """TOS view returns 404 when no TOS is set"""
+        response = self.client.get(reverse('misago:terms-of-service'))
+        self.assertEqual(response.status_code, 404)
+
+    def test_301_on_link_tos(self):
+        """TOS view returns 302 redirect when link is set"""
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='http://test.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        response = self.client.get(reverse('misago:terms-of-service'))
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], 'http://test.com')
+
+    def test_200_on_link_tos(self):
+        """TOS view returns 200 when custom tos content is set"""
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            title='Test ToS',
+            text='Lorem ipsum dolor',
+            is_active=True,
+        )
+
+        response = self.client.get(reverse('misago:terms-of-service'))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Test ToS')
+        self.assertContains(response, 'Lorem ipsum dolor')

+ 2 - 2
misago/legal/urls.py → misago/legal/urls/__init__.py

@@ -1,9 +1,9 @@
 from django.conf.urls import url
 
-from .views import privacy_policy, terms_of_service
+from misago.legal.views import privacy_policy, terms_of_service
 
 
 urlpatterns = [
     url(r'^privacy-policy/$', privacy_policy, name='privacy-policy'),
     url(r'^terms-of-service/$', terms_of_service, name='terms-of-service'),
-]
+]

+ 8 - 0
misago/legal/urls/api.py

@@ -0,0 +1,8 @@
+from django.conf.urls import url
+
+from misago.legal.api import submit_agreement
+
+
+urlpatterns = [
+    url(r'^submit-agreement/(?P<pk>\d+)/$', submit_agreement, name='submit-agreement'),
+]

+ 66 - 0
misago/legal/utils.py

@@ -0,0 +1,66 @@
+from hashlib import md5
+
+from django.conf import settings
+from django.utils.encoding import force_bytes
+
+from misago.core.cache import cache
+from misago.markup import common_flavour
+
+from .models import Agreement, UserAgreement
+
+
+def set_agreement_as_active(agreement, commit=False):
+    agreement.is_active = True
+    queryset = Agreement.objects.filter(type=agreement.type).exclude(pk=agreement.pk)
+    queryset.update(is_active=False)
+
+    if commit:
+        agreement.save(update_fields=['is_active'])
+        Agreement.objects.invalidate_cache()
+
+
+def get_required_user_agreement(user, agreements):
+    if user.is_anonymous:
+        return None
+
+    for agreement in agreements.values():
+        if agreement['id'] not in user.agreements:
+            try:
+                return Agreement.objects.get(id=agreement['id'])
+            except Agreement.DoesNotExist:
+                # possible stale cache
+                Agreement.invalidate_cache()
+    
+    return None
+
+
+def get_parsed_agreement_text(request, agreement):
+    if not agreement.text:
+        return None
+
+    cache_name = 'misago_legal_%s_%s' % (agreement.pk, agreement.last_modified_on or '')
+    cached_content = cache.get(cache_name)
+
+    unparsed_content = agreement.text
+
+    checksum_source = force_bytes('%s:%s' % (unparsed_content, settings.SECRET_KEY))
+    unparsed_checksum = md5(checksum_source).hexdigest()
+
+    if cached_content and cached_content.get('checksum') == unparsed_checksum:
+        return cached_content['parsed']
+    else:
+        parsed = common_flavour(request, None, unparsed_content)['parsed_text']
+        cached_content = {
+            'checksum': unparsed_checksum,
+            'parsed': parsed,
+        }
+        cache.set(cache_name, cached_content)
+        return cached_content['parsed']
+
+
+def save_user_agreement_acceptance(user, agreement, commit=False):
+    user.agreements.append(agreement.id)
+    UserAgreement.objects.create(agreement=agreement, user=user)
+
+    if commit:
+        user.save(update_fields=['agreements'])

+ 0 - 69
misago/legal/views.py

@@ -1,69 +0,0 @@
-from hashlib import md5
-
-from django.http import Http404
-from django.shortcuts import redirect, render
-from django.utils.encoding import force_bytes
-from django.utils.translation import ugettext as _
-
-from misago.conf import settings
-from misago.core.cache import cache
-from misago.markup import common_flavour
-
-
-def get_parsed_content(request, setting_name):
-    cache_name = 'misago_legal_%s' % setting_name
-    cached_content = cache.get(cache_name)
-
-    unparsed_content = settings.get_lazy_setting(setting_name)
-
-    checksum_source = force_bytes('%s:%s' % (unparsed_content, settings.SECRET_KEY))
-    unparsed_checksum = md5(checksum_source).hexdigest()
-
-    if cached_content and cached_content.get('checksum') == unparsed_checksum:
-        return cached_content['parsed']
-    else:
-        parsed = common_flavour(request, None, unparsed_content)['parsed_text']
-        cached_content = {
-            'checksum': unparsed_checksum,
-            'parsed': parsed,
-        }
-        cache.set(cache_name, cached_content)
-        return cached_content['parsed']
-
-
-def privacy_policy(request):
-    if not (settings.privacy_policy or settings.privacy_policy_link):
-        raise Http404()
-
-    if settings.privacy_policy_link:
-        return redirect(settings.privacy_policy_link)
-
-    parsed_content = get_parsed_content(request, 'privacy_policy')
-
-    return render(
-        request, 'misago/privacy_policy.html', {
-            'id': 'privacy-policy',
-            'title': settings.privacy_policy_title or _("Privacy policy"),
-            'link': settings.privacy_policy_link,
-            'body': parsed_content,
-        }
-    )
-
-
-def terms_of_service(request):
-    if not (settings.terms_of_service or settings.terms_of_service_link):
-        raise Http404()
-
-    if settings.terms_of_service_link:
-        return redirect(settings.terms_of_service_link)
-
-    parsed_content = get_parsed_content(request, 'terms_of_service')
-
-    return render(
-        request, 'misago/terms_of_service.html', {
-            'id': 'terms-of-service',
-            'title': settings.terms_of_service_title or _("Terms of service"),
-            'link': settings.terms_of_service_link,
-            'body': parsed_content,
-        }
-    )

+ 1 - 0
misago/legal/views/__init__.py

@@ -0,0 +1 @@
+from .legal import privacy_policy, terms_of_service

+ 87 - 0
misago/legal/views/admin.py

@@ -0,0 +1,87 @@
+from django.contrib import messages
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from misago.admin.views import generic
+
+from misago.legal.forms import AgreementForm, SearchAgreementsForm
+from misago.legal.models import Agreement
+from misago.legal.utils import set_agreement_as_active
+
+
+class AgreementAdmin(generic.AdminBaseMixin):
+    root_link = 'misago:admin:users:agreements:index'
+    model = Agreement
+    form = AgreementForm
+    templates_dir = 'misago/admin/agreements'
+    message_404 = _("Requested agreement does not exist.")
+
+    def handle_form(self, form, request, target):
+        form.save()
+        
+        if self.message_submit:
+            messages.success(request, self.message_submit % {'title': target.get_final_title()})
+
+
+class AgreementsList(AgreementAdmin, generic.ListView):
+    items_per_page = 30
+    ordering = [
+        ('-id', _("From newest")),
+        ('id', _("From oldest")),
+    ]
+    search_form = SearchAgreementsForm
+    selection_label = _('With agreements: 0')
+    empty_selection_label = _('Select agreements')
+    mass_actions = ({
+        'action': 'delete',
+        'icon': 'fa fa-times',
+        'name': _('Delete agreements'),
+        'confirmation': _('Are you sure you want to delete those agreements?')
+    }, )
+
+    def get_queryset(self):
+        qs = super(AgreementsList, self).get_queryset()
+        return qs.select_related()
+
+    def action_delete(self, request, items):
+        items.delete()
+        Agreement.objects.invalidate_cache()
+        messages.success(request, _("Selected agreements have been deleted."))
+
+
+class NewAgreement(AgreementAdmin, generic.ModelFormView):
+    message_submit = _('New agreement "%(title)s" has been saved.')
+    
+    def handle_form(self, form, request, target):
+        super(NewAgreement, self).handle_form(form, request, target)
+
+        form.instance.set_created_by(request.user)
+        form.instance.save()
+
+
+class EditAgreement(AgreementAdmin, generic.ModelFormView):
+    message_submit = _('Agreement "%(title)s" has been edited.')
+
+    def handle_form(self, form, request, target):
+        super(EditAgreement, self).handle_form(form, request, target)
+
+        form.instance.last_modified_on = timezone.now()
+        form.instance.set_last_modified_by(request.user)
+        form.instance.save()
+
+
+class DeleteAgreement(AgreementAdmin, generic.ButtonView):
+    def button_action(self, request, target):
+        target.delete()
+        Agreement.objects.invalidate_cache()
+        message = _('Agreement "%(title)s" has been deleted.')
+        messages.success(request, message % {'title': target.get_final_title()})
+
+
+class SetAgreementAsActive(AgreementAdmin, generic.ButtonView):
+    def button_action(self, request, target):
+        set_agreement_as_active(target, commit=True)
+
+        message = _('Agreement "%(title)s" has been set as active for type "%(type)s".')
+        targets_names = {'title': target.get_final_title(), 'type': target.get_type_display()}
+        messages.success(request, message % targets_names)

+ 34 - 0
misago/legal/views/legal.py

@@ -0,0 +1,34 @@
+from django.shortcuts import get_object_or_404, redirect, render
+
+from misago.legal.models import Agreement
+from misago.legal.utils import get_parsed_agreement_text
+
+
+def legal_view(request, agreement_type):
+    agreement = get_object_or_404(
+        Agreement, type=agreement_type, is_active=True
+    )
+
+    if agreement.link:
+        return redirect(agreement.link)
+
+    template_name = 'misago/{}.html'.format(agreement_type)
+    agreement_text = get_parsed_agreement_text(request, agreement)
+
+    return render(
+        request,
+        template_name,
+        {
+            'title': agreement.get_final_title(),
+            'link': agreement.link,
+            'text': agreement_text,
+        }
+    )
+
+
+def privacy_policy(request):
+    return legal_view(request, Agreement.TYPE_PRIVACY)
+
+
+def terms_of_service(request):
+    return legal_view(request, Agreement.TYPE_TOS)

BIN
misago/locale/en/LC_MESSAGES/django.mo


+ 719 - 292
misago/locale/en/LC_MESSAGES/django.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr ""
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr ""
 
@@ -142,17 +142,17 @@ msgstr ""
 msgid "Action is not allowed."
 msgstr ""
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr ""
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr ""
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr ""
 
@@ -171,11 +171,11 @@ msgstr ""
 msgid "Category roles"
 msgstr ""
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr ""
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr ""
 
@@ -183,7 +183,7 @@ msgstr ""
 msgid "Optional description explaining category intented purpose."
 msgstr ""
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr ""
 
@@ -200,7 +200,7 @@ msgstr ""
 msgid "Only members with valid permissions can post in closed categories."
 msgstr ""
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -213,7 +213,7 @@ msgstr ""
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr ""
 
@@ -321,8 +321,7 @@ msgid "Move child categories to"
 msgstr ""
 
 #: categories/forms.py:247 categories/forms.py:262
-#: templates/misago/acl_debug.html:15
-#: templates/misago/admin/roles/list.html:16
+#: templates/misago/acl_debug.html:15 templates/misago/admin/roles/list.html:16
 msgid "Role"
 msgstr ""
 
@@ -511,7 +510,7 @@ msgstr ""
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr ""
 
@@ -525,7 +524,7 @@ msgstr ""
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr ""
 
@@ -623,7 +622,72 @@ msgstr ""
 msgid "Value is too long."
 msgstr ""
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr ""
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr ""
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr ""
 
@@ -666,7 +730,7 @@ msgstr ""
 msgid "Policy title"
 msgstr ""
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr ""
@@ -690,21 +754,85 @@ msgid ""
 msgstr ""
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr ""
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr ""
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr ""
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr ""
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr ""
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr ""
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -714,15 +842,15 @@ msgstr ""
 msgid "Quoted message:"
 msgstr ""
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr ""
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr ""
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr ""
 
@@ -818,6 +946,98 @@ msgstr ""
 msgid "Your account can't be activated at this time."
 msgstr ""
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr ""
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr ""
@@ -828,9 +1048,7 @@ msgstr ""
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
 
 #: templates/misago/admin/attachments/list.html:53
@@ -881,11 +1099,6 @@ msgstr ""
 msgid "Availability"
 msgstr ""
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr ""
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr ""
@@ -898,19 +1111,6 @@ msgstr ""
 msgid "Files"
 msgstr ""
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr ""
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr ""
@@ -927,12 +1127,12 @@ msgid "New ban"
 msgstr ""
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr ""
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr ""
 
@@ -940,8 +1140,8 @@ msgstr ""
 msgid "Ban"
 msgstr ""
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr ""
 
@@ -954,10 +1154,6 @@ msgstr ""
 msgid "Never"
 msgstr ""
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr ""
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr ""
@@ -970,10 +1166,6 @@ msgstr ""
 msgid "Are you sure you want to remove this ban?"
 msgstr ""
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr ""
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr ""
@@ -1097,11 +1289,6 @@ msgstr ""
 msgid "New role"
 msgstr ""
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr ""
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr ""
@@ -1134,6 +1321,61 @@ msgstr ""
 msgid "Change settings"
 msgstr ""
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1284,44 +1526,73 @@ msgstr ""
 msgid "Administration Home"
 msgstr ""
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr ""
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr ""
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr ""
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr ""
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr ""
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr ""
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr ""
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr ""
 
@@ -1349,7 +1620,7 @@ msgstr ""
 msgid "Username or e-mail"
 msgstr ""
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr ""
@@ -1394,17 +1665,12 @@ msgid "Display and visibility"
 msgstr ""
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
-#: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: templates/misago/userslists/active_posters.html:106 users/forms/admin.py:238
 msgid "Rank"
 msgstr ""
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr ""
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr ""
@@ -1446,7 +1712,7 @@ msgid "No user roles are currently defined."
 msgstr ""
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr ""
 
@@ -1454,14 +1720,11 @@ msgstr ""
 msgid "Ban selected users:"
 msgstr ""
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
 msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr ""
 
@@ -1555,6 +1818,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr ""
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1562,51 +1837,55 @@ msgstr ""
 msgid "New user"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
+msgid "E-mail"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:20
-msgid "E-mail"
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr ""
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr ""
 
@@ -1813,6 +2092,30 @@ msgstr ""
 msgid "Set new password"
 msgstr ""
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -1898,7 +2201,7 @@ msgstr ""
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
 msgstr ""
 
@@ -1914,7 +2217,7 @@ msgstr ""
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
 msgstr ""
 
@@ -2145,7 +2448,7 @@ msgstr ""
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr ""
 
@@ -2192,7 +2495,7 @@ msgstr[1] ""
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr ""
 
@@ -2220,7 +2523,7 @@ msgid "This error is caused by invalid post content manipulation."
 msgstr ""
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr ""
 
@@ -2248,7 +2551,7 @@ msgid "%(username)s has no followers."
 msgstr ""
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr ""
 
@@ -2335,7 +2638,7 @@ msgid "%(username)s started no threads."
 msgstr ""
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr ""
 
@@ -2362,6 +2665,20 @@ msgstr ""
 msgid "%(username)s's username was never changed."
 msgstr ""
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr ""
@@ -2478,10 +2795,6 @@ msgstr ""
 msgid "By %(event_by)s on %(event_on)s."
 msgstr ""
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr ""
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2760,11 +3073,11 @@ msgstr ""
 msgid "Attachment types"
 msgstr ""
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr ""
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr ""
 
@@ -2772,24 +3085,24 @@ msgstr ""
 msgid "Uploaded image was corrupted or invalid."
 msgstr ""
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr ""
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
 msgstr ""
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
 "%(upload)s)."
 msgstr ""
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr ""
 
@@ -2805,12 +3118,12 @@ msgstr ""
 msgid "You can't like posts in this category."
 msgstr ""
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr ""
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr ""
 
@@ -2823,7 +3136,7 @@ msgstr ""
 msgid "You don't have permission to remove \"%(attachment)s\" attachment."
 msgstr ""
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -2895,11 +3208,11 @@ msgstr[1] ""
 msgid "One or more users could not be found: %(usernames)s"
 msgstr ""
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr ""
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr ""
 
@@ -2977,7 +3290,7 @@ msgstr ""
 msgid "One or more threads to update could not be found."
 msgstr ""
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr ""
 
@@ -3013,7 +3326,7 @@ msgstr ""
 msgid "File type"
 msgstr ""
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr ""
 
@@ -3037,10 +3350,6 @@ msgstr ""
 msgid "Maximum allowed uploaded file size"
 msgstr ""
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr ""
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr ""
@@ -3647,7 +3956,7 @@ msgstr ""
 msgid "Can see threads"
 msgstr ""
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4539,6 +4848,14 @@ msgstr ""
 msgid "You have to make a choice."
 msgstr ""
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -4673,23 +4990,13 @@ msgstr ""
 msgid "Requested attachment could not be found."
 msgstr ""
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr ""
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr ""
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr ""
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr ""
 
@@ -4757,33 +5064,37 @@ msgid ""
 "post."
 msgstr ""
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr ""
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr ""
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr ""
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr ""
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr ""
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr ""
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr ""
 
@@ -4853,15 +5164,15 @@ msgstr ""
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr ""
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr ""
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr ""
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr ""
 
@@ -4886,38 +5197,54 @@ msgstr ""
 msgid "You don't have permission to see other users name history."
 msgstr ""
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr ""
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr ""
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr ""
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr ""
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr ""
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr ""
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr ""
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr ""
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr ""
@@ -4930,11 +5257,11 @@ msgstr ""
 msgid "Change email or password"
 msgstr ""
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -4991,295 +5318,295 @@ msgstr ""
 msgid "Edit the user from Misago admin panel"
 msgstr ""
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr ""
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr ""
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr ""
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr ""
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr ""
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to "
 "admin Django modules."
 msgstr ""
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr ""
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
 msgstr ""
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr ""
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
 msgstr ""
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr ""
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
 msgstr ""
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr ""
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr ""
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
 msgstr ""
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr ""
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
 msgstr ""
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
 msgstr ""
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr ""
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr ""
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr ""
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr ""
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr ""
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr ""
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr ""
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr ""
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
 msgstr[0] ""
 msgstr[1] ""
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
 msgstr ""
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr ""
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr ""
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr ""
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr ""
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr ""
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr ""
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr ""
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr ""
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr ""
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr ""
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr ""
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr ""
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
 msgstr ""
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr ""
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example "
 "\"GM\" or \"Dev\"."
 msgstr ""
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
 msgstr ""
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr ""
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr ""
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr ""
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
 msgstr ""
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr ""
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr ""
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
-msgid "Usernames"
+#: users/forms/admin.py:471
+msgid "Optional message displayed to users instead of default one."
 msgstr ""
 
-#: users/forms/admin.py:465 users/forms/admin.py:580
-msgid "E-mails"
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
+msgid "Message can't be longer than 1000 characters."
 msgstr ""
 
-#: users/forms/admin.py:466
-msgid "E-mail domains"
+#: users/forms/admin.py:478 users/forms/admin.py:550
+msgid "Team message"
 msgstr ""
 
-#: users/forms/admin.py:467
-msgid "IP addresses"
+#: users/forms/admin.py:481 users/forms/admin.py:553
+msgid "Optional ban message for moderators and administrators."
 msgstr ""
 
-#: users/forms/admin.py:468
-msgid "First segment of IP addresses"
+#: users/forms/admin.py:490
+msgid "Leave this field empty for set bans to never expire."
 msgstr ""
 
-#: users/forms/admin.py:469
-msgid "First two segments of IP addresses"
+#: users/forms/admin.py:499 users/forms/admin.py:593
+msgid "Usernames"
 msgstr ""
 
-#: users/forms/admin.py:476
-msgid "Optional message displayed to users instead of default one."
+#: users/forms/admin.py:500 users/forms/admin.py:594
+msgid "E-mails"
 msgstr ""
 
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
-msgid "Message can't be longer than 1000 characters."
+#: users/forms/admin.py:501
+msgid "E-mail domains"
 msgstr ""
 
-#: users/forms/admin.py:483 users/forms/admin.py:536
-msgid "Team message"
+#: users/forms/admin.py:507
+msgid "IP addresses"
 msgstr ""
 
-#: users/forms/admin.py:486 users/forms/admin.py:539
-msgid "Optional ban message for moderators and administrators."
+#: users/forms/admin.py:508
+msgid "First segment of IP addresses"
 msgstr ""
 
-#: users/forms/admin.py:495
-msgid "Leave this field empty for set bans to never expire."
+#: users/forms/admin.py:509
+msgid "First two segments of IP addresses"
 msgstr ""
 
-#: users/forms/admin.py:501
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr ""
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr ""
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5287,61 +5614,84 @@ msgid ""
 "existing users."
 msgstr ""
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr ""
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
 "beginning with \"83.\"."
 msgstr ""
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr ""
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr ""
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr ""
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr ""
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr ""
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr ""
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr ""
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr ""
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr ""
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr ""
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr ""
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case "
+"insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr ""
@@ -5398,18 +5748,27 @@ msgid ""
 "request new password."
 msgstr ""
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr ""
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr ""
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr ""
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -5633,7 +5992,7 @@ msgid "Enter each answer in new line. Answers are case-insensitive."
 msgstr ""
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr ""
 
@@ -5646,47 +6005,59 @@ msgstr ""
 msgid "Members"
 msgstr ""
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr ""
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr ""
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr ""
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr ""
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr ""
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr ""
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr ""
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr ""
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr ""
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr ""
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6038,11 +6409,7 @@ msgstr ""
 msgid "Join IP"
 msgstr ""
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr ""
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr ""
@@ -6084,14 +6451,30 @@ msgstr ""
 msgid "New e-mail is same as current one."
 msgstr ""
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available "
 "for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6187,6 +6570,42 @@ msgstr ""
 msgid "Ban \"%(name)s\" has been removed."
 msgstr ""
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr ""
@@ -6236,92 +6655,100 @@ msgstr ""
 msgid "Rank \"%(name)s\" has been made default."
 msgstr ""
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr ""
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr ""
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr ""
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr ""
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr ""
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr ""
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr ""
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr ""
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
 msgstr ""
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr ""
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr ""
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr ""
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr ""
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr ""
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr ""
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr ""
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr ""
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr ""
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr ""
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr ""
 

BIN
misago/locale/en/LC_MESSAGES/djangojs.mo


+ 285 - 206
misago/locale/en/LC_MESSAGES/djangojs.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -39,7 +39,29 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
 msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
@@ -48,11 +70,12 @@ msgstr ""
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr ""
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr ""
 
@@ -70,9 +93,10 @@ msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr ""
 
@@ -194,8 +218,8 @@ msgid "Generate my individual avatar"
 msgstr ""
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr ""
 
@@ -263,39 +287,39 @@ msgstr ""
 msgid "Emphase selection"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr ""
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr ""
 
@@ -327,7 +351,7 @@ msgstr ""
 msgid "Error uploading %(filename)s"
 msgstr ""
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr ""
 
@@ -344,7 +368,7 @@ msgid "Protected"
 msgstr ""
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr ""
 
@@ -379,7 +403,7 @@ msgid ""
 "deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr ""
 
@@ -394,6 +418,7 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr ""
 
@@ -419,8 +444,7 @@ msgstr ""
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr ""
 
@@ -453,7 +477,7 @@ msgstr[1] ""
 msgid "Your new username is same as current one."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr ""
 
@@ -462,15 +486,15 @@ msgid "Your username has been changed successfully."
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr ""
 
@@ -506,7 +530,54 @@ msgstr ""
 msgid "Delete my account"
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr ""
 
@@ -589,7 +660,7 @@ msgstr ""
 msgid "Threads I reply to"
 msgstr ""
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr ""
 
@@ -639,7 +710,7 @@ msgstr ""
 msgid "Repeat password"
 msgstr ""
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr ""
 
@@ -821,21 +892,21 @@ msgid_plural "%(votes)s votes."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr ""
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr ""
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] ""
@@ -853,9 +924,8 @@ msgstr ""
 msgid "See votes"
 msgstr ""
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr ""
 
@@ -865,8 +935,7 @@ msgid ""
 msgstr ""
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr ""
 
@@ -929,27 +998,27 @@ msgstr ""
 msgid "See previous change"
 msgstr ""
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr ""
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr ""
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr ""
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr ""
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr ""
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr ""
 
@@ -971,7 +1040,7 @@ msgstr ""
 msgid "Are you sure you want to discard changes?"
 msgstr ""
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr ""
 
@@ -1003,11 +1072,12 @@ msgstr ""
 msgid "You have to enter at least one recipient."
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr ""
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr ""
 
@@ -1015,12 +1085,13 @@ msgstr ""
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr ""
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr ""
 
@@ -1028,35 +1099,35 @@ msgstr ""
 msgid "Are you sure you want to discard thread?"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr ""
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr ""
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr ""
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1103,12 +1174,12 @@ msgstr[0] ""
 msgstr[1] ""
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr ""
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr ""
 
@@ -1130,10 +1201,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr ""
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr ""
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr ""
 
@@ -1222,23 +1289,23 @@ msgstr ""
 msgid "Post has been deleted."
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] ""
@@ -1248,17 +1315,15 @@ msgstr[1] ""
 msgid "Changes history"
 msgstr ""
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr ""
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr ""
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr ""
 
@@ -1271,6 +1336,7 @@ msgid "Move post"
 msgstr ""
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr ""
 
@@ -1320,7 +1386,7 @@ msgid "Close thread"
 msgstr ""
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr ""
 
@@ -1368,32 +1434,32 @@ msgid_plural "%(users)s and %(likes)s other users like this."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr ""
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr ""
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr ""
 
@@ -1441,69 +1507,68 @@ msgstr ""
 msgid "%(username)s is not sharing any details with others."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr ""
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr ""
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr ""
 
@@ -1594,6 +1659,7 @@ msgid "Joined %(joined_on)s"
 msgstr ""
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr ""
 
@@ -1647,41 +1713,41 @@ msgstr ""
 msgid "Avatar controls"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr ""
 
@@ -1747,75 +1813,74 @@ msgstr ""
 msgid "Register"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr ""
 
@@ -1880,40 +1945,41 @@ msgstr ""
 msgid "Enter at least two characters to search users."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to it."
+"%(username)s, your account has been created and you have been signed in to "
+"it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -1922,6 +1988,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -1942,7 +2012,7 @@ msgid "Edit title"
 msgstr ""
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr ""
 
@@ -1950,7 +2020,7 @@ msgstr ""
 msgid "Unapproved posts"
 msgstr ""
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] ""
@@ -1971,19 +2041,19 @@ msgid ""
 "reversible!"
 msgstr ""
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr ""
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr ""
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr ""
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr ""
 
@@ -2043,37 +2113,37 @@ msgstr ""
 msgid "Unpin"
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr ""
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr ""
 
@@ -2111,15 +2181,15 @@ msgstr ""
 msgid "Disabled"
 msgstr ""
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr ""
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr ""
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr ""
 
@@ -2151,11 +2221,11 @@ msgstr ""
 msgid "Options"
 msgstr ""
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr ""
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2176,7 +2246,7 @@ msgstr ""
 msgid "Change subscription"
 msgstr ""
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr ""
 
@@ -2256,7 +2326,7 @@ msgstr ""
 msgid "Unpin threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr ""
 
@@ -2292,7 +2362,7 @@ msgstr ""
 msgid "One or more threads could not be deleted:"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2304,61 +2374,61 @@ msgid ""
 "to it."
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr ""
 
@@ -2374,7 +2444,8 @@ msgstr ""
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr ""
 
@@ -2382,67 +2453,67 @@ msgstr ""
 msgid "Are you sure you want to sign out?"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr ""
 
@@ -2456,19 +2527,19 @@ msgstr[1] ""
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr ""
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr ""
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr ""
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr ""
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] ""
@@ -2500,25 +2571,25 @@ msgstr ""
 msgid "no"
 msgstr ""
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
 msgstr ""
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr ""
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr ""
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr ""
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr ""
 
@@ -2554,15 +2625,23 @@ msgstr ""
 msgid "You don't have permission to perform this action."
 msgstr ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2572,7 +2651,7 @@ msgid_plural ""
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2582,23 +2661,23 @@ msgid_plural ""
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] ""
 msgstr[1] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural "Valid password must be at least %(limit_value)s characters long."
 msgstr[0] ""

BIN
misago/locale/es/LC_MESSAGES/django.mo


+ 726 - 303
misago/locale/es/LC_MESSAGES/django.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Carlos López <carlos@vocalaze.com>, 2017\n"
 "Language-Team: Spanish (https://www.transifex.com/misago/teams/65369/es/)\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr "Permisos"
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr "Roles de usuario"
 
@@ -142,17 +142,17 @@ msgstr "Debes seleccionar uno o más artículos."
 msgid "Action is not allowed."
 msgstr "La acción no está permitida"
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr "Anticuado %(current)s! (último: %(latest)s)"
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr "¡Actualizado! (%(current)s)"
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr "Error al conectarse a la API de GitHub. Inténtalo más tarde."
 
@@ -171,11 +171,11 @@ msgstr "Jerarquía de categorías"
 msgid "Category roles"
 msgstr "Roles de categoría"
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr "Nombre"
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr "Descripción"
 
@@ -183,7 +183,7 @@ msgstr "Descripción"
 msgid "Optional description explaining category intented purpose."
 msgstr "Descripción opcional que explica el propósito de la categoría."
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr "Clase CSS"
 
@@ -205,7 +205,7 @@ msgstr ""
 "Solo los miembros con permisos válidos pueden publicar en categorías "
 "cerradas."
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -218,7 +218,7 @@ msgstr ""
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr "Hilos"
 
@@ -533,7 +533,7 @@ msgstr "La autenticación de solicitud no es válida."
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr "Sí"
 
@@ -547,7 +547,7 @@ msgstr "Sí"
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr "No"
 
@@ -652,7 +652,72 @@ msgstr "El valor debe contener caracteres alfanuméricos."
 msgid "Value is too long."
 msgstr "El valor es demasiado largo."
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr "Tipo"
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr "Título"
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr "INFORMACIÓN LEGAL"
 
@@ -702,7 +767,7 @@ msgstr ""
 msgid "Policy title"
 msgstr "Título de la política"
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr "Política de privacidad"
@@ -731,21 +796,85 @@ msgstr ""
 "Misago está disponible para formatear."
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr "Nota"
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr "Se muestra un mensaje corto en el pie de página del foro."
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr "Pie de página del foro"
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr "Términos del servicio"
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr "Desde el más nuevo"
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr "Desde el más antiguo"
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -755,15 +884,15 @@ msgstr "%(title)s ha escrito:"
 msgid "Quoted message:"
 msgstr "Mensaje citado:"
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr "Personal"
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr "Contacto"
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr "Dirección IP"
 
@@ -872,6 +1001,98 @@ msgstr "Activación fallida"
 msgid "Your account can't be activated at this time."
 msgstr "Tu cuenta no puede ser activada en este momento."
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr "Configuración básica"
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr "Editar"
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr "Prohibiciones de búsqueda"
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr "Adjuntar archivo"
@@ -882,12 +1103,8 @@ msgstr "Hilo"
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
-"%(filetype)s, %(size)s, subido por %(uploader)s %(uploaded_on)s desde "
-"%(uploader_ip)s."
 
 #: templates/misago/admin/attachments/list.html:53
 #: templates/misago/admin/attachmenttypes/list.html:47
@@ -938,11 +1155,6 @@ msgstr "Opciones Básicas"
 msgid "Availability"
 msgstr "Disponibilidad"
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr "Tipo"
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr "Extensiones"
@@ -955,19 +1167,6 @@ msgstr "Mimetypes"
 msgid "Files"
 msgstr "Archivos"
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr "Editar"
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr "No hay tipos de archivos adjuntos actualmente definidos."
@@ -984,12 +1183,12 @@ msgid "New ban"
 msgstr "Nueva prohibición"
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr "Configuración de prohibición"
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr "Mensaje"
 
@@ -997,8 +1196,8 @@ msgstr "Mensaje"
 msgid "Ban"
 msgstr "Ban"
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr "Expira el"
 
@@ -1011,10 +1210,6 @@ msgstr "%(check_type)s, sólo registro"
 msgid "Never"
 msgstr "Nunca"
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr "Eliminar"
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr ""
@@ -1029,10 +1224,6 @@ msgstr "No hay prohibiciones establecidas actualmente."
 msgid "Are you sure you want to remove this ban?"
 msgstr "¿Seguro que desea eliminar este ticket?"
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr "Prohibiciones de búsqueda"
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr "Administración de Misago"
@@ -1165,11 +1356,6 @@ msgstr "¿Estás seguro de que quieres abandonar los cambios?"
 msgid "New role"
 msgstr "Nuevo rol"
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr "Configuración básica"
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr "Papel de categoría"
@@ -1207,6 +1393,61 @@ msgstr ""
 msgid "Change settings"
 msgstr "Cambiar ajustes"
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr "Usuario"
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr "Estado"
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr "Avatar"
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1363,44 +1604,73 @@ msgstr "Último"
 msgid "Administration Home"
 msgstr "Administración Inicio"
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr "Versión de Misago"
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr "Comprobar Versión"
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr "Esta característica requiere el módulo \"paquete\" de python."
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr "Contenido de BD"
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr "Mensajes"
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr "Usuarios"
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr "Usuarios inactivos"
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr "Comprobando..."
 
@@ -1428,7 +1698,7 @@ msgstr "Vuelve a intentarlo."
 msgid "Username or e-mail"
 msgstr "Nombre de usuario o e-mail"
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr "Contraseña"
@@ -1473,17 +1743,13 @@ msgid "Display and visibility"
 msgstr "Pantalla y visibilidad"
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
 #: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: users/forms/admin.py:238
 msgid "Rank"
 msgstr "Rango"
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr "Título"
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr "Especial"
@@ -1525,7 +1791,7 @@ msgid "No user roles are currently defined."
 msgstr "No hay roles de usuario actualmente definidos."
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr "Usuarios de prohibición"
 
@@ -1533,14 +1799,11 @@ msgstr "Usuarios de prohibición"
 msgid "Ban selected users:"
 msgstr "Prohibir usuarios seleccionados:"
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
-msgstr "Avatar"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
+msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr "Establecer prohibiciones"
 
@@ -1637,6 +1900,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr "No hay mensaje de personal disponible."
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1644,52 +1919,56 @@ msgstr "No hay mensaje de personal disponible."
 msgid "New user"
 msgstr "Nuevo usuario"
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
-msgstr "Usuario"
-
-#: templates/misago/admin/users/list.html:20
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr "Unido"
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr "Está deshabilitado por el administrador"
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr "Requiere activación por parte del administrador"
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr "Tiene que activar la cuenta"
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr "Súper administrador"
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr "Administrador"
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr "Editar usuario"
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr ""
 "No se han encontrado usuarios que coincidan con los criterios de búsqueda."
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr "Búqueda de Usuarios"
 
@@ -1905,6 +2184,30 @@ msgstr "Para cambiar la contraseña haz click aquí:"
 msgid "Set new password"
 msgstr "Establece la nueva contraseña"
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -2002,11 +2305,9 @@ msgstr ""
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
 msgstr ""
-"%(user)s, estás recibiendo este mensaje porque %(poster)s ha respondido a un"
-" hilo %(thread)s al que estás suscrito."
 
 #: templates/misago/emails/thread/reply.html:14
 #: templates/misago/emails/thread/reply.txt:10
@@ -2020,11 +2321,9 @@ msgstr "Ir a la respuesta"
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
 msgstr ""
-"%(user)s, estás recibiendo este mensaje porque %(poster)s ha respondido a un"
-" hilo \"%(thread)s\" al que estás suscrito."
 
 #: templates/misago/errorpages/403.html:40
 msgid "This page is not available."
@@ -2269,7 +2568,7 @@ msgstr "Ver resultados"
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr "Detalles del baneo"
 
@@ -2316,7 +2615,7 @@ msgstr[1] "Empezó %(threads)s hilos."
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr "Detalles"
 
@@ -2346,7 +2645,7 @@ msgstr ""
 "mensaje."
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr "Seguidores"
 
@@ -2374,7 +2673,7 @@ msgid "%(username)s has no followers."
 msgstr "%(username)s no tiene seguidores."
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr "Seguidos"
 
@@ -2461,7 +2760,7 @@ msgid "%(username)s started no threads."
 msgstr "%(username)s no ha iniciado ningún hilo."
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr "Historial de nombres de usuario"
 
@@ -2488,6 +2787,20 @@ msgstr "Tu nombre de usuario nunca fue cambiado."
 msgid "%(username)s's username was never changed."
 msgstr "%(username)s no ha cambiado nunca su nombre de usuario."
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr "Buscar sitio"
@@ -2604,10 +2917,6 @@ msgstr "Oculto por%(hidden_by)s en %(hidden_on)s."
 msgid "By %(event_by)s on %(event_on)s."
 msgstr "Por %(event_by)s en %(event_on)s."
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr "IP guardada"
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2892,11 +3201,11 @@ msgstr "Adjuntos"
 msgid "Attachment types"
 msgstr "tipos de archivos adjuntos"
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr "No tienes permiso para subir nuevos archivos."
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr "No se ha subido ningún archivo"
 
@@ -2904,11 +3213,11 @@ msgstr "No se ha subido ningún archivo"
 msgid "Uploaded image was corrupted or invalid."
 msgstr "La imagen cargada estaba dañada o no es válida."
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr "No puedes subir archivos de este tipo."
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
@@ -2916,7 +3225,7 @@ msgstr ""
 "No puede cargar archivos mayores que %(limit)s (su archivo tiene "
 "%(upload)s)."
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
@@ -2925,7 +3234,7 @@ msgstr ""
 "No puede cargar archivos de este tipo mayores que %(limit)s (su archivo "
 "tiene %(upload)s)."
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr "El registro de ediciones no está disponible para esta publicación."
 
@@ -2941,12 +3250,12 @@ msgstr "No puedes mover publicaciones en este hilo."
 msgid "You can't like posts in this category."
 msgstr "No te pueden gustar las publicaciones en esta categoría."
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr "La aprobación del contenido no se puede revertir."
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr "No se encontraron una o más publicaciones para actualizar."
 
@@ -2959,7 +3268,7 @@ msgstr "No puedes dividir publicaciones de este hilo."
 msgid "You don't have permission to remove \"%(attachment)s\" attachment."
 msgstr "No tienes permiso para eliminar el archivo adjunto \"%(attachment)s\"."
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -3041,11 +3350,11 @@ msgstr[1] ""
 msgid "One or more users could not be found: %(usernames)s"
 msgstr "Uno o más usuarios no pudieron ser encontrados: %(usernames)s"
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr "Debes introducir un mensaje."
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr "Debes darle un título al hilo."
 
@@ -3128,7 +3437,7 @@ msgstr "Este usuario ya es propietario del hilo."
 msgid "One or more threads to update could not be found."
 msgstr "Uno o más hilos para actualizar no se pudieron encontrar."
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr "Ya hay una encuesta en este hilo."
 
@@ -3164,7 +3473,7 @@ msgstr "Nombre de archivo contiene"
 msgid "File type"
 msgstr "Tipo de archivo"
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr "Estado"
 
@@ -3188,10 +3497,6 @@ msgstr "Extensiones de archivo"
 msgid "Maximum allowed uploaded file size"
 msgstr "Tamaño de archivo máximo permitido"
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr "Estado"
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr "Limitar las cargas a"
@@ -3844,7 +4149,7 @@ msgstr "Permite publicar con más frecuencia que la protección contra flood."
 msgid "Can see threads"
 msgstr "Puede ver hilos"
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4764,6 +5069,14 @@ msgstr "Una o más de las elecciones de encuesta no fueron válidas."
 msgid "You have to make a choice."
 msgstr "Tienes que hacer una elección."
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -4918,23 +5231,13 @@ msgstr "No tienes permiso para ver listas de contenido no aprobadas."
 msgid "Requested attachment could not be found."
 msgstr "No se pudo encontrar el archivo adjunto solicitado."
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr "Desde el más nuevo"
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr "Desde el más antiguo"
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr "De la A a la Z"
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr "Z a a"
 
@@ -5007,33 +5310,37 @@ msgstr ""
 "Necesita permiso para aprobar el contenido para poder acceder a la primera "
 "publicación no aprobada."
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr "Cuentas de usuario"
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr "Rangos"
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr "Prohibiciones"
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr "Activa la cuenta %(user)s de %(forum_name)s"
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr "Cambiar la contraseña de %(user)s en %(forum_name)s"
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr "El enlace del formulario no es válido. Por favor inténtalo de nuevo"
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr "Tu enlace ha expirado Por favor, solicita uno nuevo."
 
@@ -5105,17 +5412,17 @@ msgstr ""
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr "Confirmar el cambio de contraseña de %(forum_name)s"
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr ""
 "El enlace de confirmación de cambio de contraseña fue enviado a tu "
 "dirección."
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr "Los nuevos registros de usuarios están actualmente cerrados."
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr "Intenta volver a enviar el formulario."
 
@@ -5140,39 +5447,55 @@ msgstr "Error al cambiar el nombre de usuario. Por favor inténtalo de nuevo"
 msgid "You don't have permission to see other users name history."
 msgstr "No tienes permiso para ver el historial de nombres de otros usuarios."
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr "Debes iniciar sesión para realizar esta acción."
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr "No puedes cambiar avatares de otros usuarios."
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr "No puede cambiar opciones de otros usuarios."
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr "Tus opciones de foro han sido cambiadas."
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr "No puedes cambiar otros nombres de usuarios."
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr "No puedes cambiar las firmas de otros usuarios."
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr "No puedes cambiar las contraseñas de otros usuarios."
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr ""
 "No puedes cambiar las direcciones de correo electrónico de otros usuarios."
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr "Editar detalles"
@@ -5185,11 +5508,11 @@ msgstr "Cambiar nombre de usuario"
 msgid "Change email or password"
 msgstr "Cambiar e-mail o contraseña"
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -5247,27 +5570,27 @@ msgstr "Editar permisos y grupos"
 msgid "Edit the user from Misago admin panel"
 msgstr "Edita al usuario desde el panel de administración de Misago"
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr "Nombre de usuario"
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr "Título personalizado"
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr "Dirección de correo electrónico"
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr "Todos los miembros registrados deben tener el rol \"Miembro \"."
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr "Es administrador"
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to"
@@ -5278,11 +5601,11 @@ msgstr ""
 "necesitará permisos adicionales asignados para administrar los módulos de "
 "Django."
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr "Es superusuario"
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
@@ -5291,11 +5614,11 @@ msgstr ""
 "Además del acceso del sitio de administración, las superadminas también "
 "pueden cambiar los niveles de administración de otros miembros."
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr "Está activo"
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
@@ -5303,11 +5626,11 @@ msgstr ""
 "Designa si este usuario debe tratarse como activo. Desactivar esto es una "
 "forma no destructiva de eliminar cuentas de usuario."
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr "Mensaje del staff"
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
@@ -5315,15 +5638,15 @@ msgstr ""
 "Mensaje opcional para los miembros del equipo del foro que explica por qué "
 "se ha inhabilitado la cuenta del usuario."
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr "Cambiar la contraseña a"
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr "Bloquear avatar"
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
@@ -5331,12 +5654,12 @@ msgstr ""
 "Establecer esto evitará que el usuario cambie su avatar, y restablecerá su "
 "avatar a uno generado por procedimiento."
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr "Mensaje de usuario"
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
@@ -5344,7 +5667,7 @@ msgstr ""
 "Mensaje opcional para el usuario que explica por qué está prohibido cambiar "
 "el avatar."
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
@@ -5352,52 +5675,52 @@ msgstr ""
 "Mensaje opcional para los miembros del equipo del foro explicando por qué el"
 " usuario está prohibido para cambiar el avatar."
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr "Contenido de la firma"
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr "Bloquear firma"
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr "Establecer esto evitará que el usuario realice cambios en su firma."
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr ""
 "Mensaje opcional para el usuario explicando por qué su firma de está "
 "bloqueada."
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr ""
 "Mensaje opcional para los miembros del equipo explicando por qué la firma "
 "del usuario está bloqueada."
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr "Oculta la presencia"
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr "¿Quién puede agregar usuarios a hilos privados?"
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr "Hilos repetidos"
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
 msgstr[0] "La firma no puede ser más larga que el %(limit)s caracter."
 msgstr[1] "La firma no puede ser más larga que el %(limit)s caracteres."
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
@@ -5405,61 +5728,61 @@ msgstr ""
 "Los rangos se usan para agrupar y distinguir usuarios. También se usan para "
 "agregar permisos a grupos de usuarios."
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr "Roles"
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr ""
 "Roles individuales de este usuario Todos los usuarios deben tener el rol "
 "\"miembro\"."
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr "El nombre de usuario comienza con"
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr "El correo electrónico comienza con"
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr "Los campos de perfil contienen"
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr "Solo inactivo"
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr "Deshabilitado solo"
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr "Admins solo"
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr "Todos los rangos"
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr "Todos los roles"
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr "Tiene rango"
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr "Tiene un rol"
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
@@ -5467,11 +5790,11 @@ msgstr ""
 "Nombre breve y descriptivo de todos los usuarios con este rango. \"The "
 "Team\" o \"Game Masters\" son buenos ejemplos."
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr "Título del usuario"
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example"
 " \"GM\" or \"Dev\"."
@@ -5479,7 +5802,7 @@ msgstr ""
 "Versión opcional y singular del nombre de rango mostrado por los nombres de "
 "usuario. Por ejemplo \"GM\" o \"Dev\"."
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
@@ -5487,21 +5810,21 @@ msgstr ""
 "Descripción opcional que explica la función o el estado de los miembros "
 "distinguidos con este rango."
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr "Rank puede otorgar roles adicionales a los usuarios que lo utilizan."
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr ""
 "Clase css opcional agregada al contenido que pertenece al propietario del "
 "rango."
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr "Dar una pestaña de rango dedicada en la lista de usuarios"
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
@@ -5510,71 +5833,71 @@ msgstr ""
 " detectables por otros a través de la página dedicada en la lista de "
 "usuarios del foro."
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr "Este nombre colisiona con otro rango."
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr "Valores para prohibir"
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
-msgid "Usernames"
-msgstr "Nombres de Usuario"
-
-#: users/forms/admin.py:465 users/forms/admin.py:580
-msgid "E-mails"
-msgstr "Correos electrónicos"
-
-#: users/forms/admin.py:466
-msgid "E-mail domains"
-msgstr "Dominios de correo electrónico"
-
-#: users/forms/admin.py:467
-msgid "IP addresses"
-msgstr "Dirección(es) IP "
-
-#: users/forms/admin.py:468
-msgid "First segment of IP addresses"
-msgstr "Primer segmento de direcciones IP"
-
-#: users/forms/admin.py:469
-msgid "First two segments of IP addresses"
-msgstr "Primeros dos segmentos de direcciones IP"
-
-#: users/forms/admin.py:476
+#: users/forms/admin.py:471
 msgid "Optional message displayed to users instead of default one."
 msgstr ""
 "Mensaje opcional que se muestra a los usuarios en lugar de uno "
 "predeterminado."
 
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
 msgid "Message can't be longer than 1000 characters."
 msgstr "El mensaje no puede tener más de 1000 caracteres."
 
-#: users/forms/admin.py:483 users/forms/admin.py:536
+#: users/forms/admin.py:478 users/forms/admin.py:550
 msgid "Team message"
 msgstr "Mensaje de equipo"
 
-#: users/forms/admin.py:486 users/forms/admin.py:539
+#: users/forms/admin.py:481 users/forms/admin.py:553
 msgid "Optional ban message for moderators and administrators."
 msgstr "Mensaje de prohibición opcional para moderadores y administradores."
 
-#: users/forms/admin.py:495
+#: users/forms/admin.py:490
 msgid "Leave this field empty for set bans to never expire."
 msgstr ""
 "Deja este campo vacío para que las prohibiciones establecidas nunca expiren."
 
+#: users/forms/admin.py:499 users/forms/admin.py:593
+msgid "Usernames"
+msgstr "Nombres de Usuario"
+
+#: users/forms/admin.py:500 users/forms/admin.py:594
+msgid "E-mails"
+msgstr "Correos electrónicos"
+
 #: users/forms/admin.py:501
+msgid "E-mail domains"
+msgstr "Dominios de correo electrónico"
+
+#: users/forms/admin.py:507
+msgid "IP addresses"
+msgstr "Dirección(es) IP "
+
+#: users/forms/admin.py:508
+msgid "First segment of IP addresses"
+msgstr "Primer segmento de direcciones IP"
+
+#: users/forms/admin.py:509
+msgid "First two segments of IP addresses"
+msgstr "Primeros dos segmentos de direcciones IP"
+
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr "Tipo de cheque"
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr "Restringe esta prohibición a los registros"
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5586,11 +5909,11 @@ msgstr ""
 "como los de proveedores de correo electrónico recientemente comprometidos, "
 "sin dañar a los usuarios existentes."
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr "Valor prohibido"
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
@@ -5601,51 +5924,74 @@ msgstr ""
 "prohíba el valor \"83. * \" Prohibirá todas las direcciones IP que comiencen"
 " con \"83. \"."
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr "El valor prohibido no puede tener más de 250 caracteres."
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr ""
 "Mensaje opcional que se muestra al usuario en lugar del predeterminado."
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr "Deje este campo vacío para que esta prohibición nunca expire."
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr "El valor prohibido es demasiado vago."
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr "Todas las prohibiciones"
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr "IPs"
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr "El valor prohibido comienza con"
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr "Sólo registro"
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr "Todos"
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr "Activo"
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr "Expirado"
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case"
+" insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr "Rellena ambos campos."
@@ -5705,18 +6051,27 @@ msgstr ""
 "El administrador debe activar tu cuenta antes de que pueda solicitar una "
 "nueva contraseña."
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr "Este usuario no está permitido."
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr "Esta dirección de correo electrónico no está permitida."
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr "No se permiten nuevos registros desde esta dirección IP."
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -5953,7 +6308,7 @@ msgstr ""
 "entre mayúsculas y minúsculas."
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr "Equipo del foro"
 
@@ -5966,48 +6321,60 @@ msgstr "Equipo"
 msgid "Members"
 msgstr "Miembros"
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr "El usuario debe tener una dirección de correo electrónico."
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr "Notificar"
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr "Notificar mediante e-mail"
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr "Todos"
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr "Usuarios que sigo"
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr "Nadie"
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr "se unió a"
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr "estado del personal"
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr ""
 "Designa si el usuario puede iniciar sesión en sitios de administración."
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr "activo"
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6385,11 +6752,7 @@ msgstr "Este no es un identificador de Twitter válido."
 msgid "Join IP"
 msgstr "IP de sesión"
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr "Última IP"
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr "¡Bienvenido a %(forum_name)s!"
@@ -6431,14 +6794,30 @@ msgstr "Debes ingresar una nueva dirección de correo electrónico."
 msgid "New e-mail is same as current one."
 msgstr "El nuevo correo electrónico es el mismo que el actual."
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available"
 " for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6542,6 +6921,42 @@ msgstr "Prohibir \"%(name)s\" ha sido editado."
 msgid "Ban \"%(name)s\" has been removed."
 msgstr "Prohibir \"%(name)s\" ha sido eliminado."
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr "El rango solicitado no existe."
@@ -6591,39 +7006,43 @@ msgstr "El rango \"%(name)s\" ya está predeterminado."
 msgid "Rank \"%(name)s\" has been made default."
 msgstr "El rango \"%(name)s\" se ha establecido de manera predeterminada."
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr "Los carteles más grandes"
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr "Los carteles más pequeños"
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr "Con los usuarios: 0"
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr "Seleccionar usuarios"
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr "Activar cuentas"
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr "Eliminar cuentas"
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr "¿Seguro que quieres eliminar los usuarios seleccionados?"
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr "Borrar todo"
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
@@ -6631,54 +7050,58 @@ msgstr ""
 "¿Seguro que quieres eliminar los usuarios seleccionados? Esto también "
 "eliminará todo el contenido asociado a sus cuentas."
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr "Debes seleccionar usuarios inactivos."
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr "Tu cuenta en %(forum_name)s ha sido activada"
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr "Las cuentas de los usuarios seleccionados se han activado."
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr "%(user)s es super administrador y no puede ser baneado."
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr "Los usuarios seleccionados han sido baneados."
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr "No puedes borrarte a ti mismo."
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr "%(user)s es admin y no se puede eliminar."
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr "Los usuarios seleccionados han sido eliminados."
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr "Nuevo usuario \"%(user)s\" ha sido registrado."
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr "Usuario \"%(user)s\" ha sido editado."
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr "No se puede acceder a esta acción directamente."
 

BIN
misago/locale/es/LC_MESSAGES/djangojs.mo


+ 285 - 207
misago/locale/es/LC_MESSAGES/djangojs.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-20 21:20+0200\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Javier Lorenzo <javichio@gmail.com>, 2017\n"
 "Language-Team: Spanish (https://www.transifex.com/misago/teams/65369/es/)\n"
@@ -39,8 +39,30 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
-msgstr "Registrándote, aceptas los términos y condiciones de uso."
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
+msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
@@ -48,11 +70,12 @@ msgstr "Registrándote, aceptas los términos y condiciones de uso."
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr "Cerrar"
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr "Añadir participante"
 
@@ -70,9 +93,10 @@ msgstr "Usuario a añadir"
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr "Cancelar"
 
@@ -199,8 +223,8 @@ msgid "Generate my individual avatar"
 msgstr "Generar avatar con tu inicial"
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr "Ok"
 
@@ -268,39 +292,39 @@ msgstr "Insertar código"
 msgid "Emphase selection"
 msgstr "Añadir énfasis a la selección"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr "Insertar regla horizontal"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr "Añadir enlace a la imagen"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr "Añadir etiqueta de imagen (opcional)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr "Insertar imagen"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr "Añadir dirección de enlace"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr "Añadir etiqueta de enlace (opcional)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr "Insertar enlace"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr "Añadir autor de la cita, utiliza @ para mencionar"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr "Añadir cita"
 
@@ -332,7 +356,7 @@ msgstr "Deshacer eliminar"
 msgid "Error uploading %(filename)s"
 msgstr "Error subiendo %(filename)s"
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr "Descartar"
 
@@ -349,7 +373,7 @@ msgid "Protected"
 msgstr "Protegido"
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr "Proteger"
 
@@ -384,7 +408,7 @@ msgid ""
 " deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr "Encuesta"
 
@@ -399,6 +423,7 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr "Mezclar hilos"
 
@@ -426,8 +451,7 @@ msgstr "Escrito por %(poster)s en %(posted_on)s en %(category)s."
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr "%(title)s, ha entrado en %(joined_on)s"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr "Cambiar nombre de usuario"
 
@@ -463,7 +487,7 @@ msgstr[1] ""
 msgid "Your new username is same as current one."
 msgstr "El nombre propuesto es igual al anterior."
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr "Nuevo nombre de usuario"
 
@@ -472,15 +496,15 @@ msgid "Your username has been changed successfully."
 msgstr "Tu nombre de usuario se ha cambiado correctamente."
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr "Cambia tus opciones"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr "Eliminar cuenta"
 
@@ -516,7 +540,54 @@ msgstr ""
 msgid "Delete my account"
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr "Tus detalles han sido actualizados."
 
@@ -601,7 +672,7 @@ msgstr "Hilos que he creado"
 msgid "Threads I reply to"
 msgstr "Hilos en los que he contestado"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr "Cambiar e-mail o contraseña"
 
@@ -651,7 +722,7 @@ msgstr "Nueva contraseña"
 msgid "Repeat password"
 msgstr "Repetir contraseña"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr "Cambiar contraseña olvidada"
 
@@ -840,21 +911,21 @@ msgid_plural "%(votes)s votes."
 msgstr[0] "%(votes)s voto"
 msgstr[1] "%(votes)s votos"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr "Los votos son públicos."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] "%(votes)s voto, %(proc)s% del total."
 msgstr[1] "%(votes)s votos, %(proc)s% del total."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr "Tu elección."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] "%(votes)s vez que el usuario votó por esta opción."
@@ -872,9 +943,8 @@ msgstr "Vota"
 msgid "See votes"
 msgstr "Ver votos"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr "Editar"
 
@@ -886,8 +956,7 @@ msgstr ""
 "deshacer."
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr "Eliminar"
 
@@ -953,29 +1022,29 @@ msgstr "El mensaje ha sido revertido al estado anterior."
 msgid "See previous change"
 msgstr "Ver cambio anterior"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr "Por %(edited_by)s %(edited_on)s."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr "El contenido de este mensaje no puede ser mostrado."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr ""
 "Este error está causado por una manipulación inválida del contenido del "
 "post."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr "posteado %(posted_on)s"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr "Usuario eliminado"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr "Ver mensaje"
 
@@ -997,7 +1066,7 @@ msgstr "Ningún usuario ha enlazado este mensaje."
 msgid "Are you sure you want to discard changes?"
 msgstr "¿Estás seguro de querer descartar los cambios?"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr "Debes introducir un mensaje."
 
@@ -1029,11 +1098,12 @@ msgstr "¿Estás seguro de querer descartar el hilo privado?"
 msgid "You have to enter at least one recipient."
 msgstr "Debes introducir al menos un destinatario."
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr "Debes darle un título al hilo."
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr "Tu hilo ha sido publicado."
 
@@ -1041,12 +1111,13 @@ msgstr "Tu hilo ha sido publicado."
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr "Nombres de usuario separados por comas, ejemplo: Pepito, Josefa"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr "Título del hilo"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr "Publicar hilo"
 
@@ -1054,35 +1125,35 @@ msgstr "Publicar hilo"
 msgid "Are you sure you want to discard thread?"
 msgstr "¿Estás seguro de querer descartar el hilo?"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr "Cerrado"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr "Abierto"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr "Oculto"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr "Público"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr "No fijado"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr "Fijado localmente"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1145,12 +1216,12 @@ msgstr[1] ""
 " %(show_value)s)."
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr "Ocultar"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr "Mostrar"
 
@@ -1174,10 +1245,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr "Por %(event_by)s %(events_on)s."
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr "IP guardada"
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr "El título del hilo se ha cambiado de %(old_title)s."
 
@@ -1268,23 +1335,23 @@ msgstr ""
 msgid "Post has been deleted."
 msgstr "El mensaje ha sido eliminado"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr "Enlace permanente al mensaje:"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr "Enlace permanente"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] "El mensaje ha sido editado %(edits)s vez."
@@ -1294,17 +1361,15 @@ msgstr[1] "El mensaje ha sido editado %(edits)s veces."
 msgid "Changes history"
 msgstr "Historial de cambios"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr "Aprobar"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr "Mover"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr "Separar"
 
@@ -1317,6 +1382,7 @@ msgid "Move post"
 msgstr "Mover mensaje"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr "Debes introducir el enlace a otro hilo."
 
@@ -1366,7 +1432,7 @@ msgid "Close thread"
 msgstr "Cerrar hilo"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr "Categoría"
 
@@ -1417,32 +1483,32 @@ msgid_plural "%(users)s and %(likes)s other users like this."
 msgstr[0] "%(users)s y %(likes)s otro usuario dio like aquí."
 msgstr[1] "%(users)s y %(likes)s otro usuario dio like aquí."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr "Liked"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr "Like"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr "Responder"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr "Nuevo mensaje"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr "Nuevos"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr "Este mensaje está protegido y no puede ser editado."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr "protegido"
 
@@ -1490,69 +1556,68 @@ msgstr "No compartes detalles con otros."
 msgid "%(username)s is not sharing any details with others."
 msgstr "%(username)s no comparte detalles con otros."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr "Detalles"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr "Los detalles de %(username)s han sido actualizados."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr "No has iniciado ningún hilo."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr "%(username)s no ha iniciado ningún hilo."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] "Has iniciado %(threads)s hilo."
 msgstr[1] "Has iniciado %(threads)s hilos."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] "%(username)s ha iniciado %(threads)s hilo."
 msgstr[1] "%(username)s ha iniciado %(threads)s hilos."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr "Cargando..."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr "Hilos"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr "No has publicado mensajes."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr "%(username)s no ha publicado mensajes."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] "Has publicado %(posts)s mensaje."
 msgstr[1] "Has publicado %(posts)s mensajes"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] "%(username)s ha publicado %(posts)s mensaje."
 msgstr[1] "%(username)s ha publicado %(posts)s mensajes."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr "Mensajes"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr "Mostrar más (%(more)s)"
 
@@ -1643,6 +1708,7 @@ msgid "Joined %(joined_on)s"
 msgstr "Registrado %(joined_on)s"
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr "Moderación"
 
@@ -1702,45 +1768,45 @@ msgstr ""
 msgid "Avatar controls"
 msgstr "Controles de avatar"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr "El nombre de usuario ha sido cambiado."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr ""
 "La cuenta de %(username)s's, los hilos, los mensajes y otro contenido ha "
 "sido eliminado."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr ""
 "La cuenta de %(username)s ha sido eliminada y otro contenido ha sido "
 "ocultado."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr "Eliminar %(username)s"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr "Por favor, espera... (%(countdown)ss)"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr "Contenido de usuario"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr "Borrar conjuntamente con la cuenta del usuario"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr "Ocultar tras eliminar la cuenta del usuario"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr "Volver a la lista de usuarios"
 
@@ -1808,32 +1874,32 @@ msgstr "Se han cerrado los registros debido a un error inesperado."
 msgid "Register"
 msgstr "Registro"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr "Nombre de usuario"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr "E-mail"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr "Contraseña"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr "Registrar cuenta"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
@@ -1841,7 +1907,7 @@ msgstr ""
 "%(username)s, tu cuenta ha sido creada, pero requiere ser activada antes de "
 "que puedas iniciar sesión."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
@@ -1849,7 +1915,7 @@ msgstr ""
 "%(username)s, tu cuenta ha sido creada pero la administración del sitio debe"
 " activarla antes de que puedas iniciar sesión."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
@@ -1857,32 +1923,31 @@ msgstr ""
 "Hemos enviado un correo a %(email)s con un enlace al que deberás acceder "
 "para activar tu cuenta."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr "Te enviaremos un correo a %(email)s en su momento."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr "Registro completo"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr "Introduce un correo electrónico válido."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr "Tu correo electrónico"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr "Enviar enlace"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr "El enlace de activación ha sido enviado a %(email)s"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr "Pedir otro enlace de activación"
 
@@ -1949,41 +2014,41 @@ msgstr "No existen usuarios que coincidan con tu criterio de búsqueda."
 msgid "Enter at least two characters to search users."
 msgstr "Introduce al menos dos caracteres para buscar usuarios."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr "Rellena ambos campos."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr "Activar cuenta"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr "Nombre de usuario o e-mail"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr "¿Contraseña olvidada?"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to "
+"%(username)s, your account has been created and you have been signed in to "
 "it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -1992,6 +2057,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -2012,7 +2081,7 @@ msgid "Edit title"
 msgstr "Editar título"
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr "No aprobados"
 
@@ -2020,7 +2089,7 @@ msgstr "No aprobados"
 msgid "Unapproved posts"
 msgstr "Mensajes no aprobados"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] "%(replies)s respuesta"
@@ -2046,19 +2115,19 @@ msgstr ""
 "¿Estás seguro de que quieres eliminar los mensajes seleccionados? Esta "
 "opción no puede deshacerse."
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr "Mezclar"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr "Desproteger"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr "%(username)s en %(posted_on)s"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr "Uno o más mensajes no pueden ser cambiados:"
 
@@ -2118,19 +2187,19 @@ msgstr "Fijado localmente"
 msgid "Unpin"
 msgstr "No fijar"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr "Mezclar hilo"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr "El hilo ha sido mezclado con otro."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr "Enlace al hilo con el que quieres mezclar"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
@@ -2138,19 +2207,19 @@ msgstr ""
 "La tarea de mezclado eliminará el hilo y su contenido y lo moverá al hilo "
 "que especifiques a continuación."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr "Mover hilo"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr "No puedes mover este hilo en este momento."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr "El hilo se ha movido."
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr "Nueva categoría"
 
@@ -2188,15 +2257,15 @@ msgstr "Habilitado"
 msgid "Disabled"
 msgstr "Deshabilitado"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr "Desuscribirse"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr "Suscribirse"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr "Notificaciones por email"
 
@@ -2228,11 +2297,11 @@ msgstr "Último mensaje"
 msgid "Options"
 msgstr "Opciones"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr "Añadir encuesta"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2253,7 +2322,7 @@ msgstr ""
 msgid "Change subscription"
 msgstr "Cambiar suscripción"
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr "Crear hilo"
 
@@ -2333,7 +2402,7 @@ msgstr "Fijar los hilos localmente"
 msgid "Unpin threads"
 msgstr "Soltar hilo"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr "Mover hilos"
 
@@ -2369,7 +2438,7 @@ msgstr "Moderar hilos"
 msgid "One or more threads could not be deleted:"
 msgstr "Uno o más hilos no pueden ser eliminados:"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2384,11 +2453,11 @@ msgstr ""
 "Necesitas permisos para empezar hilos en la categoría para poder mezclar "
 "hilos en ela."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr "Los hilos seleccionados se han movido."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
@@ -2396,51 +2465,51 @@ msgstr ""
 "Necesitas permisos para empezar hilos en la categoría para poder mover hilos"
 " a ella"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr "Seleccionar todo"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr "Cancelar selección"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr "Todo"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr "Todos los hilos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr "Míos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr "Mis hilos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr "Nuevos hilos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr "No leídos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr "Hilos no leídos"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr "Suscrito"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr "Hilos a los que estás suscrito"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr "Contenido no aprobado"
 
@@ -2456,7 +2525,8 @@ msgstr "Inicia sesión o registrate para comenzar a participar en los foros."
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr "Hilos privados"
 
@@ -2464,67 +2534,67 @@ msgstr "Hilos privados"
 msgid "Are you sure you want to sign out?"
 msgstr "¿Estás seguro de que quieres cerrar sesión?"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr "Ver tu perfil"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr "Cambiar opciones"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr "Cambiar avatar"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr "Cerrar sesión"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr "%(username)s ha sido banneado hasta %(ban_expires)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr "%(username)s ha sido baneado"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr "%(username)s está ocultando su presencia"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr "%(username)s está online (oculto)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr "La última actividad de %(username)s el %(last_click)s (oculto)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr "%(username)s está conectado"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr "La última actividad de %(username)s ha sido %(last_click)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr "Baneado"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr "En línea (oculto)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr "Desconectado (oculto)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr "En línea"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr "Desconectado"
 
@@ -2538,19 +2608,19 @@ msgstr[1] "%(followers)s seguidores"
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr "Los usuarios no han escrito en los últimos %(days)s days."
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr "Rango"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr "Mensajes de rango"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr "Mensajes totales"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] "%(posters)s es el forero más activo del último %(days)s día."
@@ -2582,7 +2652,7 @@ msgstr "sí"
 msgid "no"
 msgstr "no"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
@@ -2590,19 +2660,19 @@ msgstr ""
 "Los hilos privados son hilos en los que puedes hablar con los participantes "
 "que tú elijas, ellos serán los únicos que podrán leer y escribir."
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr "No participas en ningún hilo privado."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr "Se ha perdido la conexión con la aplicación."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr "Enlace de acción inválido."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr "Ups, parece que algo ha ido mal..."
 
@@ -2638,15 +2708,23 @@ msgstr "Estás haciendo una encuesta, ¿quieres descartarla?"
 msgid "You don't have permission to perform this action."
 msgstr "No tienes permisos para realizar esta acción."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr "Has sido baneado"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr "Se requiere este campo."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2660,7 +2738,7 @@ msgstr[1] ""
 "Asegúrate de que este valor tiene al menos %(limit_value)s caracteres (it "
 "has %(show_value)s)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2674,14 +2752,14 @@ msgstr[1] ""
 "Asegúrate de que el valor es al menos %(limit_value)s caracteres (es de "
 "%(show_value)s)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] "El nombre de usuario debe tener al menos %(limit_value)s caracter."
 msgstr[1] ""
 "El nombre de usuario debe tener al menos %(limit_value)s caracteres."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] ""
@@ -2689,13 +2767,13 @@ msgstr[0] ""
 msgstr[1] ""
 "El nombre de usuario no puede ser mayor de %(limit_value)s caracteres."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr ""
 "El nombre de usuario sólo puede contener caracteres latinos, letras y "
 "dígitos."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural ""
 "Valid password must be at least %(limit_value)s characters long."

BIN
misago/locale/fr/LC_MESSAGES/django.mo


+ 726 - 303
misago/locale/fr/LC_MESSAGES/django.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: carine hejl <carinelg@yahoo.fr>, 2017\n"
 "Language-Team: French (https://www.transifex.com/misago/teams/65369/fr/)\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr "Autorisations"
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr " Rôles d'utilisateurs"
 
@@ -143,17 +143,17 @@ msgstr "Vous devez sélectionner au moins un élément."
 msgid "Action is not allowed."
 msgstr "Cette action n'est pas autorisée."
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr "Caduc : %(current)s ! (dernier : %(latest)s)"
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr "Mis à jour ! (%(current)s)"
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr "Échec de la connexion à l'API GitHub. Réessayez plus tard."
 
@@ -172,11 +172,11 @@ msgstr "Hiérarchie des catégories"
 msgid "Category roles"
 msgstr "Rôles de catégories"
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr "Nom"
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr "Description"
 
@@ -184,7 +184,7 @@ msgstr "Description"
 msgid "Optional description explaining category intented purpose."
 msgstr "Description facultative expliquant l'objectif de la catégorie."
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr "Classe CSS"
 
@@ -204,7 +204,7 @@ msgstr "Catégorie fermée"
 msgid "Only members with valid permissions can post in closed categories."
 msgstr "Seul les membres autorisés peuvent écrire dans une catégorie close."
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -217,7 +217,7 @@ msgstr "Seul les membres autorisés peuvent écrire dans une catégorie close."
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr "Fils de discussion"
 
@@ -539,7 +539,7 @@ msgstr "Demande d'authentification invalide."
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr "Oui"
 
@@ -553,7 +553,7 @@ msgstr "Oui"
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr "Non"
 
@@ -656,7 +656,72 @@ msgstr "La valeur doit contenir des caractères alpha-numériques."
 msgid "Value is too long."
 msgstr "La valeur est trop longue."
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr "Type"
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr "Titre"
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr "Informations légales"
 
@@ -706,7 +771,7 @@ msgstr ""
 msgid "Policy title"
 msgstr "Titre de la politique"
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr "Politique de confidentialite"
@@ -735,21 +800,85 @@ msgstr ""
 "les balises Misago sont disponibles pour la mise en forme."
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr "Note de bas-de-page"
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr "Message court affiché en bas-de-page."
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr "Pied-de-page du forum"
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr "Termes de service"
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr "Du plus récent"
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr "Du plus ancien"
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -759,15 +888,15 @@ msgstr "%(title)s a écrit :"
 msgid "Quoted message:"
 msgstr "Message cité :"
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr "Personnel"
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr "Contact"
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr "Adresse IP"
 
@@ -878,6 +1007,98 @@ msgstr "Échec de l'activation"
 msgid "Your account can't be activated at this time."
 msgstr "Votre compte ne peut pas être activé pour le moment."
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr "Parametres par defaut"
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr "Modifier"
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr "Supprimer"
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr "Rechercher un bannissement"
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr "Pièces jointes"
@@ -888,12 +1109,8 @@ msgstr "Fil de discussion"
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
-"%(filetype)s, %(size)s, télédéposé par %(uploader)s %(uploaded_on)s depuis "
-"%(uploader_ip)s."
 
 #: templates/misago/admin/attachments/list.html:53
 #: templates/misago/admin/attachmenttypes/list.html:47
@@ -943,11 +1160,6 @@ msgstr "Options par défaut"
 msgid "Availability"
 msgstr "Disponibilité"
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr "Type"
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr "Extensions"
@@ -960,19 +1172,6 @@ msgstr "type MIME"
 msgid "Files"
 msgstr "Fichiers"
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr "Modifier"
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr "Aucun type de pièce jointe n'est actuellement défini."
@@ -989,12 +1188,12 @@ msgid "New ban"
 msgstr "Nouveau bannissement"
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr "Paramètres d'exclusion"
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr "Messages"
 
@@ -1002,8 +1201,8 @@ msgstr "Messages"
 msgid "Ban"
 msgstr "Bannir"
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr "Expire le"
 
@@ -1016,10 +1215,6 @@ msgstr "%(check_type)s, enregistrement seulement"
 msgid "Never"
 msgstr "Jamais"
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr "Supprimer"
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr ""
@@ -1033,10 +1228,6 @@ msgstr "Aucun bannissement prononcé."
 msgid "Are you sure you want to remove this ban?"
 msgstr "Êtes-vous certain de vouloir supprimer ce bannissement ?"
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr "Rechercher un bannissement"
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr "Administration de Misago"
@@ -1170,11 +1361,6 @@ msgstr "Êtes-vous sûr de vouloir abandonner les changements ?"
 msgid "New role"
 msgstr "Nouveau rôle"
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr "Parametres par defaut"
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr "Rôle de la catégorie."
@@ -1212,6 +1398,61 @@ msgstr ""
 msgid "Change settings"
 msgstr "Modifier les paramètres"
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr "Utilisateur"
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr "Statut"
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr "Avatar"
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1369,44 +1610,73 @@ msgstr "Dernier"
 msgid "Administration Home"
 msgstr "Accueil administration"
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr "Version de Misago"
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr "Vérifier la version"
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr "Cette fonction requiert le module Python nommé « packaging »"
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr "Contenu de la base de donnée"
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr "Messages"
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr "Utilisateurs"
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr "Utilisateurs inactifs"
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr "Vérification…"
 
@@ -1434,7 +1704,7 @@ msgstr "Veuillez réessayer."
 msgid "Username or e-mail"
 msgstr "Identifiant ou e-mail"
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr "Mot de passe"
@@ -1479,17 +1749,13 @@ msgid "Display and visibility"
 msgstr "Affichage et visibilité"
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
 #: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: users/forms/admin.py:238
 msgid "Rank"
 msgstr "Rang"
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr "Titre"
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr "Spécial"
@@ -1531,7 +1797,7 @@ msgid "No user roles are currently defined."
 msgstr "Aucun rôle utilisateur n'est actuellement défini."
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr "Utilisateurs bannis"
 
@@ -1539,14 +1805,11 @@ msgstr "Utilisateurs bannis"
 msgid "Ban selected users:"
 msgstr "Interdire les utilisateurs sélectionnés :"
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
-msgstr "Avatar"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
+msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr "Bannir"
 
@@ -1643,6 +1906,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr "Aucun message du staff n'est disponible."
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1650,52 +1925,56 @@ msgstr "Aucun message du staff n'est disponible."
 msgid "New user"
 msgstr "Nouvel utilisateur"
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
-msgstr "Utilisateur"
-
-#: templates/misago/admin/users/list.html:20
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr "Inscrit"
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr "est désactivé par l'administrateur"
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr "Nécessite une activation par l'administrateur"
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr "Doit activer le compte"
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr "Super-administrateur"
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr "Administrateur"
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr "Modifier l'utilisateur"
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr ""
 "Aucun utilisateur correspondant aux critères de recherche n'a été trouvé."
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr "Rechercher les utilisateurs"
 
@@ -1919,6 +2198,30 @@ msgstr ""
 msgid "Set new password"
 msgstr "Définir un nouveau mot de passe"
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -2025,11 +2328,9 @@ msgstr ""
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
 msgstr ""
-"%(user)s, vous recevez ce message car %(poster)s  a répondu au fil de "
-"discussion %(thread)s auquel vous êtes abonné."
 
 #: templates/misago/emails/thread/reply.html:14
 #: templates/misago/emails/thread/reply.txt:10
@@ -2043,11 +2344,9 @@ msgstr "Aller à la réponse"
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
 msgstr ""
-"%(user)s, vous recevez ce message car %(poster)s  a répondu au fil de "
-"discussion %(thread)s auquel vous êtes abonné."
 
 #: templates/misago/errorpages/403.html:40
 msgid "This page is not available."
@@ -2297,7 +2596,7 @@ msgstr "Voir les résultats"
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr "Details de l'interdiction"
 
@@ -2344,7 +2643,7 @@ msgstr[1] "A initié %(threads)s fils de discussion"
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr "Détails"
 
@@ -2374,7 +2673,7 @@ msgstr ""
 "message."
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr "Suiveurs"
 
@@ -2402,7 +2701,7 @@ msgid "%(username)s has no followers."
 msgstr "Personne ne suit %(username)s."
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr "Suit"
 
@@ -2489,7 +2788,7 @@ msgid "%(username)s started no threads."
 msgstr "%(username)s n'a démarré aucun fil de discussion."
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr "Historique du nom d'utilisateur"
 
@@ -2516,6 +2815,20 @@ msgstr "Votre nom d'utilisateur n'a jamais été changé."
 msgid "%(username)s's username was never changed."
 msgstr "%(username)s's nom d'utlisateur n'a jamais été changé."
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr "Rechercher sur le site"
@@ -2635,10 +2948,6 @@ msgstr "Caché par %(hidden_by)s le %(hidden_on)s."
 msgid "By %(event_by)s on %(event_on)s."
 msgstr "Par %(event_by)s le %(event_on)s."
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr "IP enregistré"
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2928,11 +3237,11 @@ msgstr "Pièces jointes"
 msgid "Attachment types"
 msgstr "Types de pièces jointes"
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr "Vous n'êtes pas autorisé⋅e à télédéposer de nouveaux fichiers."
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr "Aucun fichier télédéposé."
 
@@ -2940,11 +3249,11 @@ msgstr "Aucun fichier télédéposé."
 msgid "Uploaded image was corrupted or invalid."
 msgstr "L'image télédéposée était corrompue ou invalide."
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr "Vous ne pouvez pas télédéposer de fichiers de ce type."
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
@@ -2952,7 +3261,7 @@ msgstr ""
 "Vous ne pouvez pas télédéposer des fichiers plus larges que %(limit)s (votre"
 " fichier fait %(upload)s)"
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
@@ -2961,7 +3270,7 @@ msgstr ""
 "Vous ne pouvez pas télédéposer de fichiers de ce type plus larges "
 "que%(limit)s(votre fichier fait %(upload)s)"
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr "L'enregistrement des modifications est indisponible pour ce message."
 
@@ -2977,12 +3286,12 @@ msgstr "Vous ne pouvez pas déplacer les messages dans ce fil de discussion."
 msgid "You can't like posts in this category."
 msgstr "Vous ne pouvez pas aimer les messages dans cette catégorie."
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr "L'approbation du contenu ne peut pas être annulée."
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr "Un ou plusieurs messages à mettre à jour n'ont pas été trouvés."
 
@@ -2997,7 +3306,7 @@ msgstr ""
 "Vous n'avez pas la permission de supprimer la pièce-jointe « %(attachment)s "
 "»."
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -3083,11 +3392,11 @@ msgstr[1] ""
 msgid "One or more users could not be found: %(usernames)s"
 msgstr "Un ou plusieurs utilisateurs n’ont pas été trouvés : %(usernames)s"
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr "Vous devez saisir un message."
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr "Vous devez renseigner un titre pour le fil de discussion."
 
@@ -3175,7 +3484,7 @@ msgid "One or more threads to update could not be found."
 msgstr ""
 "Un ou plusieurs fils de discussion à mettre à jour n'ont pas été trouvés."
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr "Il y a déjà un sondage dans ce fil de discussion."
 
@@ -3211,7 +3520,7 @@ msgstr "Le nom de fichier contient"
 msgid "File type"
 msgstr "Type de fichier"
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr "État"
 
@@ -3235,10 +3544,6 @@ msgstr "Extensions de fichier"
 msgid "Maximum allowed uploaded file size"
 msgstr "Taille maximale autorisée du fichier télédéposé"
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr "Statut"
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr "Limiter les uploads à"
@@ -3932,7 +4237,7 @@ msgstr ""
 msgid "Can see threads"
 msgstr "Peut voir les fils de discussion"
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4985,6 +5290,14 @@ msgstr "Un ou plusieurs choix du sondage étaient invalides"
 msgid "You have to make a choice."
 msgstr "Vous devez faire un choix"
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -5148,23 +5461,13 @@ msgstr ""
 msgid "Requested attachment could not be found."
 msgstr "La pièce jointe requise n'a pu être trouvée"
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr "Du plus récent"
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr "Du plus ancien"
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr "A à Z"
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr "Z à A"
 
@@ -5237,34 +5540,38 @@ msgstr ""
 "Une autorisation d'approbation des contenus est nécessaire  pour pouvoir "
 "accéder aux premiers messages non approuvés."
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr "Compte utilisateur"
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr "Rangs"
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr "Bannis"
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr "Activer  %(user)s  sur les forums  %(forum_name)s "
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr ""
 "Changer les mots de passe de  %(user)s  sur les forums  %(forum_name)s "
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr "Le lien associé à ce formulaire est non valide. Veuillez réessayer."
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr "votre lien a expiré. veuillez en demander un nouveau."
 
@@ -5338,16 +5645,16 @@ msgstr ""
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr "Confirmer le changement de mot de passe sur  %(forum_name)s forums"
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr ""
 "Un lien permettant de changer de mot de passe a été envoyé à votre adresse."
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr "Les inscriptions sont closes."
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr "Veuillez resoumettre le formulaire."
 
@@ -5375,38 +5682,54 @@ msgid "You don't have permission to see other users name history."
 msgstr ""
 "Vous n'avez pas le droit d'accéder à l'historique des autres utilisateurs."
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr "Vous devez vous identifier pour réaliser cette action."
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr "Vous ne pouvez modifier les avatars des autres utilisateurs."
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr "Vous ne pouvez modifier les options des autres utilisateurs."
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr "Les options de votre forum ont été modifiées."
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr "Vous ne pouvez modifier les noms des autres utilisateurs."
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr "Vous ne pouvez modifier les identifiants des autres utilisateurs."
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr "Vous ne pouvez modifier les mots de passe des autres utilisateurs."
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr "Vous ne pouvez modifier les courriels des autres utilisateurs."
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr "Modifier les détails"
@@ -5419,11 +5742,11 @@ msgstr "Changer de nom d'utilisateur"
 msgid "Change email or password"
 msgstr "Changer le courriel ou le mot de passe"
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -5483,27 +5806,27 @@ msgid "Edit the user from Misago admin panel"
 msgstr ""
 "Modifier l'utilisateur depuis le tableau de bord d'administration Misago"
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr "Nom d'utilisateur"
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr "Titre personnalisé"
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr "Courriel"
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr "Tous les membres enregistrés doivent avoir un rôle de « Membre »."
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr "Est administrateur"
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to"
@@ -5513,11 +5836,11 @@ msgstr ""
 "site d'administration de Django est activé, cet utilisateur aura besoin "
 "d'autorisations supplémentaires pour administrer les modules Django."
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr "Est super utilisateur"
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
@@ -5526,11 +5849,11 @@ msgstr ""
 "plus de l'accès au site administrateur, les superadministrateurs peuvent "
 "aussi modifier les niveaux administrateurs des autres membres."
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr "Est actif"
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
@@ -5538,11 +5861,11 @@ msgstr ""
 "indique si cet utilisateur doit être considéré comme actif. Désactiver cette"
 " option est un moyen non destructif de supprimer les comptes utilisateurs."
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr "Message du staff"
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
@@ -5550,15 +5873,15 @@ msgstr ""
 "Message optionnel destiné aux membres de l'équipe du forum afin d'expliquer "
 "pourquoi le compte d'un utilisateur a été désactivé."
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr "Changer le mot de passe par"
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr "Verrouiller l'avatar"
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
@@ -5566,12 +5889,12 @@ msgstr ""
 "Activer cette option empêchera l'utilisateur de changer son avatar et va "
 "générer un nouvel avatar selon la procédure"
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr "Message de l'utilisateur"
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
@@ -5579,7 +5902,7 @@ msgstr ""
 "Message optionnel à l'attention d'un utilisateur pour lui expliquer pourquoi"
 " il ou elle a été empêche de changer d'avatar"
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
@@ -5587,56 +5910,56 @@ msgstr ""
 "Message optionnel destiné aux membres de l'équipe du forum afin d'expliquer "
 "pourquoi il a été interdit à un utilisateur de modifier son avatar."
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr ""
 "Indique si cet utilisateur doit être considéré comme actif. Désactiver cette"
 " option est un moyen non destructif de supprimer les comptes utilisateurs."
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr "Verrouiller la signature"
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr ""
 "Attribuer la valeur oui à ce paramètre empêchera un utilisateur de modifier "
 "son identifiant. "
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr ""
 "Message optionnel destiné à l'utilisateur pour lui expliquer pourquoi son "
 "identifiant est verrouillé."
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr ""
 "Message optionnel à l'attention des membres de l'équipe afin d'expliquer à "
 "l'utilisateur pourquoi son identifiant est verrouillé."
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr "Cache la présence"
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr "Qui peut ajouter des utilisateurs à des fils de discussion privés"
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr "Fils de discussion pour lesquels il existe des réponses"
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
 msgstr[0] "La signature peut comporter au plus %(limit)s caractère."
 msgstr[1] "La signature peut comporter au plus %(limit)s caractères."
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
@@ -5645,61 +5968,61 @@ msgstr ""
 "utilisateurs. Ils sont également utilisés pour donner des droits à des "
 "groupes d'utilisateurs."
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr "Rôles"
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr ""
 "Les rôles individuels de cet utilisateur. Tous les utilisateurs doivent "
 "avoir un rôle de « membre »."
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr "Nom d'utilisateur commençant par"
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr "Courriel commençant par"
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr "Les champs de profil contiennent"
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr "Inactifs uniquement"
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr "Désactivés uniquement"
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr "Administrateurs uniquement"
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr "Tous les rangs"
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr "Tous les rôles"
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr "A un rang"
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr "A un rôle"
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
@@ -5707,11 +6030,11 @@ msgstr ""
 "Nom court et descriptif de tous les utilisateurs avec ce rang. « L'équipe » "
 "ou  « les maîtres du jeux » sont de bons exemples."
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr "Titre d'un utilisateur"
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example"
 " \"GM\" or \"Dev\"."
@@ -5719,7 +6042,7 @@ msgstr ""
 "Version unique et optionnelle du rang affiché par les noms utilisateurs. Par"
 " exemple « GM » OU « Dev »."
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
@@ -5727,22 +6050,22 @@ msgstr ""
 "Description optionnelle expliquant la fonction ou le statut des membres "
 "distingués par ce rang."
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr ""
 "Les rangs peuvent conférer des rôles supplémentaires aux utilisateurs."
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr ""
 "La classe CSS optionnelle est ajoutée au contenu possédé par le propriétaire"
 " de ce rang."
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr "Attribuer un onglet dédié aux rangs à la liste des utilisateurs"
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
@@ -5751,73 +6074,73 @@ msgstr ""
 "plus facilement détectable par les autres via une page dédiée sur le forum à"
 " la liste des utilisateurs."
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr "Ce nom est redondant avec un autre rang."
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr "Valeurs à bannir."
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
-msgid "Usernames"
-msgstr "Noms d'utilisateurs"
-
-#: users/forms/admin.py:465 users/forms/admin.py:580
-msgid "E-mails"
-msgstr "Courriels"
-
-#: users/forms/admin.py:466
-msgid "E-mail domains"
-msgstr "Domaines des courriels"
-
-#: users/forms/admin.py:467
-msgid "IP addresses"
-msgstr "Adresses IP"
-
-#: users/forms/admin.py:468
-msgid "First segment of IP addresses"
-msgstr "Le premier segment des adresses IP"
-
-#: users/forms/admin.py:469
-msgid "First two segments of IP addresses"
-msgstr "Les deux premiers segments des adresses IP"
-
-#: users/forms/admin.py:476
+#: users/forms/admin.py:471
 msgid "Optional message displayed to users instead of default one."
 msgstr ""
 "Message optionnel adressé aux utilisateurs à la place d'un message par "
 "défaut."
 
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
 msgid "Message can't be longer than 1000 characters."
 msgstr "La longueur du message ne doit pas excéder 1000 caractères."
 
-#: users/forms/admin.py:483 users/forms/admin.py:536
+#: users/forms/admin.py:478 users/forms/admin.py:550
 msgid "Team message"
 msgstr "Message de l'équipe"
 
-#: users/forms/admin.py:486 users/forms/admin.py:539
+#: users/forms/admin.py:481 users/forms/admin.py:553
 msgid "Optional ban message for moderators and administrators."
 msgstr ""
 "Message optionnel d'interdiction pour les modérateurs et administrateurs."
 
-#: users/forms/admin.py:495
+#: users/forms/admin.py:490
 msgid "Leave this field empty for set bans to never expire."
 msgstr ""
 "Laisser ce champ vide pour établir des interdictions qui n'expireront "
 "jamais."
 
+#: users/forms/admin.py:499 users/forms/admin.py:593
+msgid "Usernames"
+msgstr "Noms d'utilisateurs"
+
+#: users/forms/admin.py:500 users/forms/admin.py:594
+msgid "E-mails"
+msgstr "Courriels"
+
 #: users/forms/admin.py:501
+msgid "E-mail domains"
+msgstr "Domaines des courriels"
+
+#: users/forms/admin.py:507
+msgid "IP addresses"
+msgstr "Adresses IP"
+
+#: users/forms/admin.py:508
+msgid "First segment of IP addresses"
+msgstr "Le premier segment des adresses IP"
+
+#: users/forms/admin.py:509
+msgid "First two segments of IP addresses"
+msgstr "Les deux premiers segments des adresses IP"
+
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr "Vérifier le type"
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr "Restreindre cette interdiction aux enregistrements"
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5829,11 +6152,11 @@ msgstr ""
 "bloquer certains enregistrements en particulier ceux provenant de courriels "
 "récemment compromis,  sans nuire aux utilisateurs existants. "
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr "Valeur interdite"
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
@@ -5843,55 +6166,78 @@ msgstr ""
 "résultats bruts. Par exemple, interdire IP pour la valeur « 83.* » interdira"
 " toutes les adresses IP commençant par « 83. »."
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr ""
 "Une valeur interdite ne peut être de taille supérieure à 250 caractères."
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr ""
 "Message optionnel adressé aux utilisateurs à la place d'un message par "
 "défaut."
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr ""
 "Laisser ce champ vide de façon à ce que cette interdiction ne se termine "
 "jamais."
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr "La valeur interdite est trop imprécise."
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr "Toutes les interdictions."
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr "Les IPs"
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr "Une valeur interdite commence par "
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr "Enregistrement uniquement"
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr "N'importe lequel"
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr "Actif"
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr "Expiré"
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case"
+" insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr "Remplissez les deux champs."
@@ -5952,20 +6298,29 @@ msgstr ""
 "Un administrateur doit d'abord activer votre compte pour que vous puissiez "
 "demander un nouveau mot de passe"
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr "Ce nom d'utilisateur n'est pas autorisé."
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr "Ce courriel n'est pas autorisé."
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr ""
 "De nouveaux enregistrements provenant de cette adresse IP ne sont pas "
 "autorisés."
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -6203,7 +6558,7 @@ msgstr ""
 "affectées par la casse."
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr "Équipe du forum"
 
@@ -6216,48 +6571,60 @@ msgstr "Équipe "
 msgid "Members"
 msgstr "Membres"
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr "L'utilisateur doit avoir un courriel."
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr "Notifier"
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr "Notifier par courriel"
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr "Tout le monde"
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr "Utilisateurs que je suis"
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr "Personne"
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr "inscrit le"
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr "Statut du staff"
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr ""
 "Indique si l'utilisateur peut se connecter sur les sites d'administration."
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr "Actif"
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6646,11 +7013,7 @@ msgstr "Ceci n'est pas un pseudo twitter valide."
 msgid "Join IP"
 msgstr "IP d'inscription"
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr "Dernière IP"
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr "Bienvenue sur le forum %(forum_name)s !"
@@ -6692,14 +7055,30 @@ msgstr "Vous devez entrer un nouveau courriel."
 msgid "New e-mail is same as current one."
 msgstr "La nouveau courriel est le même que l'actuel."
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available"
 " for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6805,6 +7184,42 @@ msgstr "Le bannissement « %(name)s » a été modifié."
 msgid "Ban \"%(name)s\" has been removed."
 msgstr "L'interdiction \"%(name)s\" a été levée."
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr "Le rang demandé n'existe pas."
@@ -6856,39 +7271,43 @@ msgstr "Le rang « %(name)s » est déjà le rang par défaut."
 msgid "Rank \"%(name)s\" has been made default."
 msgstr "Le rang « %(name)s » a été défini comme rang par défaut."
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr "Intervenants les plus actifs"
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr "intervenants les moins actifs"
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr "Avec des utilisateurs : 0"
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr "Sélectionner des utilisateurs "
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr "Activer des comptes"
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr "Supprimer des comptes"
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr "Etes vous certain de supprimer les utilisateurs sélectionnés ?"
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr "Supprimer tout"
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
@@ -6896,54 +7315,58 @@ msgstr ""
 "Etes vous certain de vouloir supprimer les utilisateurs sélectionnés ? Cela "
 "supprimera également tous les contenus associés à leurs comptes."
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr "Vous devez sélectionner des utilisateurs inactifs."
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr "Votre compte sur le forum %(forum_name)s a été activé."
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr "Les comptes des utilisateurs sélectionnés ont été activés."
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr "%(user)s est un super administrateur et ne peut être banni."
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr "Les utilisateurs sélectionnés ont été bannis."
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr "Vous ne pouvez supprimer votre compte."
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr "%(user)s est administrateur et ne peut être supprimé."
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr "Les utilisateurs sélectionnés ont été supprimés."
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr "Le nouvel utilisateur « %(user)s » a été enregistré."
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr "L'utilisateur « %(user)s » a été modifié."
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr "Il n'est pas possible de réaliser cette action directement."
 

BIN
misago/locale/fr/LC_MESSAGES/djangojs.mo


+ 285 - 207
misago/locale/fr/LC_MESSAGES/djangojs.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-20 21:20+0200\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Amandine G <amandyfriendly@msn.com>, 2017\n"
 "Language-Team: French (https://www.transifex.com/misago/teams/65369/fr/)\n"
@@ -39,8 +39,30 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
-msgstr "En vous enregistrant, vous acceptez les termes et conditions du site."
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
+msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
@@ -48,11 +70,12 @@ msgstr "En vous enregistrant, vous acceptez les termes et conditions du site."
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr "Fermer"
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr "Ajouter un participant"
 
@@ -70,9 +93,10 @@ msgstr "Utilisateur à ajouter"
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr "Annuler"
 
@@ -204,8 +228,8 @@ msgid "Generate my individual avatar"
 msgstr "Générer mon avatar personnel"
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr "Ok"
 
@@ -274,40 +298,40 @@ msgstr "Déposer le code"
 msgid "Emphase selection"
 msgstr "Accentuer la selection"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr "Insérer une barre horizontale"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr "Lien vers une image"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr "Donnez un nom à l'image (facultatif)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr "Insérer une image"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr "Adresse du lien"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr "Étiquette pour le lien (facultatif)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr "Insérer un lien"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr ""
 "Pour citer un utilisateur, précéder son nom d'utilisateur du caractère @"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr "Insérer une citation"
 
@@ -339,7 +363,7 @@ msgstr "Annuler la suppression"
 msgid "Error uploading %(filename)s"
 msgstr "Erreur pendant le télédépôt de %(filename)s"
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr "Rejeter"
 
@@ -356,7 +380,7 @@ msgid "Protected"
 msgstr "Protégé"
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr "Protéger"
 
@@ -391,7 +415,7 @@ msgid ""
 " deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr "Sondage"
 
@@ -406,6 +430,7 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr "Fusionner les fils"
 
@@ -431,8 +456,7 @@ msgstr "Posté par %(poster)s le %(posted_on)s dans %(category)s."
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr "%(title)s, membre depuis le %(joined_on)s"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr "Changer de nom d'utilisateur"
 
@@ -474,7 +498,7 @@ msgstr[1] ""
 msgid "Your new username is same as current one."
 msgstr "Votre nouveau nom d'utilisateur est identique a l'actuel."
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr "Nouveau nom d'utilisateur"
 
@@ -483,15 +507,15 @@ msgid "Your username has been changed successfully."
 msgstr "Votre nom d'utilisateur a bien été changé."
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr "Changer vos options"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr "Supprimer le compte"
 
@@ -527,7 +551,54 @@ msgstr ""
 msgid "Delete my account"
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr "Vos informations ont été mises à jour."
 
@@ -612,7 +683,7 @@ msgstr "Fils que je démarre"
 msgid "Threads I reply to"
 msgstr "Fils auxquels je réponds"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr "Changer l'email ou le mot de passe"
 
@@ -662,7 +733,7 @@ msgstr "Nouveau mot de passe"
 msgid "Repeat password"
 msgstr "Répéter le mot de passe"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr "Changer le mot de passe oublié"
 
@@ -851,21 +922,21 @@ msgid_plural "%(votes)s votes."
 msgstr[0] "%(votes)s vote."
 msgstr[1] "%(votes)s votes."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr "Les votes sont publics."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] "%(votes)s vote, %(proc)s% du total."
 msgstr[1] "%(votes)s votes, %(proc)s% du total."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr "Votre choix."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] "%(votes)s utilisateur a voté pour ce choix."
@@ -883,9 +954,8 @@ msgstr "Vote"
 msgid "See votes"
 msgstr "Voir les votes"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr "Éditer"
 
@@ -897,8 +967,7 @@ msgstr ""
 "irréversible !"
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr "Supprimer"
 
@@ -964,29 +1033,29 @@ msgstr "La version précédente de la publication a été rétablie."
 msgid "See previous change"
 msgstr "Voir le changement précédent"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr "Par %(edited_by)s le %(edited_on)s."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr "Le contenu de cette publication ne peut être affiché."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr ""
 "Cette erreur est causée par une mauvaise manipulation du contenu de la "
 "publication."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr "publié le %(posted_on)s"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr "Utilisateur supprimé"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr "Voir la publication"
 
@@ -1008,7 +1077,7 @@ msgstr "Aucun utilisateur n'a aimé cette publication."
 msgid "Are you sure you want to discard changes?"
 msgstr "Êtes-vous sûr de vouloir rejeter les modifications ?"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr "Vous devez rédiger un message."
 
@@ -1040,11 +1109,12 @@ msgstr "Êtes-vous sûr de vouloir supprimer votre fil de discussion privé ?"
 msgid "You have to enter at least one recipient."
 msgstr "Vous devez renseigner au moins un destinataire."
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr "Vous devez renseigner un titre pour le fil de discussion."
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr "Votre fil de discussion a bien été publié."
 
@@ -1052,12 +1122,13 @@ msgstr "Votre fil de discussion a bien été publié."
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr "Séparer les noms d'utilisateur par une virgule, p. ex. Danny, Lisa"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr "Titre du fil"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr "Publier le fil"
 
@@ -1065,35 +1136,35 @@ msgstr "Publier le fil"
 msgid "Are you sure you want to discard thread?"
 msgstr "Êtes-vous sûr de vouloir supprimer le fil de discussion ?"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr "Fermée"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr "Ouvert"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr "Caché"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr "Non caché"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr "Non épinglé"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr "Épinglé localement"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1156,12 +1227,12 @@ msgstr[1] ""
 "(actuellement %(show_value)s)."
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr "Cacher"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr "Afficher"
 
@@ -1185,10 +1256,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr "Par %(event_by)s %(event_on)s."
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr "IP enregistrée"
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr "Le titre du fil %(old_title)s a été modifié."
 
@@ -1280,23 +1347,23 @@ msgstr ""
 msgid "Post has been deleted."
 msgstr "La publication a été supprimée."
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr "Lien permanent vers cette publication :"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr "Lien permanent"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] "Cette publication a été modifiée %(edits)s fois."
@@ -1306,17 +1373,15 @@ msgstr[1] "Cette publication a été modifiée %(edits)s fois."
 msgid "Changes history"
 msgstr "Historique des modifications"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr "Approuver"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr "Déplacer"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr "Détacher"
 
@@ -1329,6 +1394,7 @@ msgid "Move post"
 msgstr "Déplacer la publication"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr "Vous devez entrer le lien vers l'autre fil."
 
@@ -1379,7 +1445,7 @@ msgid "Close thread"
 msgstr "Fermer le fil"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr "Catégorie"
 
@@ -1432,32 +1498,32 @@ msgid_plural "%(users)s and %(likes)s other users like this."
 msgstr[0] "%(users)s et %(likes)s autre utilisateur aime cela."
 msgstr[1] "%(users)s et %(likes)s autre utilisateurs aiment ça."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr "J'aime"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr "J'aime"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr "Citer et répondre"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr "Nouvelle publication"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr "Nouveau"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr "Cette publication est protégée et ne peut être modifiée."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr "protégé"
 
@@ -1505,69 +1571,68 @@ msgstr "Vous ne partagez aucune information avec les autres."
 msgid "%(username)s is not sharing any details with others."
 msgstr "%(username)s ne partage aucune information avec les autres"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr "Détails "
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr "Les informations de %(username)s ont été mises à jour."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr "Vous n'avez démarré aucun fil de discussion."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr "%(username)s n'a démarré aucun fil de discussion."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] "Vous avez démarré %(threads)s fil de discussion."
 msgstr[1] "Vous avez démarré %(threads)s fils de discussion."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] "%(username)s a démarré %(threads)s fil de discussion."
 msgstr[1] "%(username)s a démarré %(threads)s fils de discussion."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr "Chargement…"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr "Fils de discussion"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr "Vous n'avez posté aucun message."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr "%(username)s n'a posté aucuns messages."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] "Vous avez posté %(posts)s message."
 msgstr[1] "Vous avez posté %(posts)s messages."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] "%(username)s a posté %(posts)s message."
 msgstr[1] "%(username)s a posté %(posts)s messages."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr "Publications"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr "Montrer davantage (%(more)s)"
 
@@ -1660,6 +1725,7 @@ msgid "Joined %(joined_on)s"
 msgstr "Membre depuis %(joined_on)s"
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr "Modération"
 
@@ -1719,45 +1785,45 @@ msgstr ""
 msgid "Avatar controls"
 msgstr "Paramètres avatar"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr "L’identifiant a été modifié."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr ""
 "Le compte de %(username)s, ses fils de discussions, ses publications et "
 "autres contenus ont été supprimés."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr ""
 "Le compte utilisateur de %(username)s a été supprimé. Le contenu associé a "
 "été caché."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr "Supprimer %(username)s"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr "Merci d'attendre… (%(countdown)ss)"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr "Contenu de l'utilisateur"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr "Supprimer avec le compte de l'utilisateur"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr "Cacher après suppression du compte utilisateur"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr "Revenir à la liste utilisateur"
 
@@ -1827,32 +1893,32 @@ msgstr ""
 msgid "Register"
 msgstr "S'enregistrer"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr "Nom d'utilisateur"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr "E-mail"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr "Password"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr "Créer un compte"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
@@ -1860,7 +1926,7 @@ msgstr ""
 "%(username)s, votre compte a été crée mais vous devez l'activer avant de "
 "pouvoir vous authentifier."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
@@ -1868,7 +1934,7 @@ msgstr ""
 "%(username)s, votre compte a été crée mais un administrateur doit l'activer "
 "avant que vous puissiez vous authentifier."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
@@ -1876,32 +1942,31 @@ msgstr ""
 "Nous avons envoyé un e-mail à l'adresse %(email)s contenant un lien "
 "d'activation."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr "Nous enverrons un e-mail à %(email)s lorsque cela se produit."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr "Inscription terminée"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr "Entrez une adresse email valide."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr "Votre adresse e-mail"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr "Envoyer le lien"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr "Un lien d'activation a été envoyé à %(email)s"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr "Demander un autre lien"
 
@@ -1968,41 +2033,41 @@ msgstr "Aucun utilisateur correspondant à la recherche n'a été trouvé."
 msgid "Enter at least two characters to search users."
 msgstr "Entrez au moins deux caractères pour effectuer une recherche."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr "Renseigner les deux champs."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr "Activer le compte"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr "Nom d'utilisateur ou e-mail"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr "Mot de passe oublié ?"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to "
+"%(username)s, your account has been created and you have been signed in to "
 "it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -2011,6 +2076,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -2031,7 +2100,7 @@ msgid "Edit title"
 msgstr "Editer le titre"
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr "Non approuvé"
 
@@ -2039,7 +2108,7 @@ msgstr "Non approuvé"
 msgid "Unapproved posts"
 msgstr "Message non approuvés"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] "%(replies)s réponse"
@@ -2065,19 +2134,19 @@ msgstr ""
 "Êtes-vous sûr de vouloir supprimer les publications sélectionnées ? Cette "
 "action est irréversible !"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr "Fusionner"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr "Non protégé"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr "%(username)s le %(posted_on)s"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr "Un ou plusieurs messages n'ont pas pu être modifiés :"
 
@@ -2139,19 +2208,19 @@ msgstr "Épingler localement"
 msgid "Unpin"
 msgstr "Détacher"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr "Fusionner le fil"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr "Le thread a été fusionné avec un autre."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr "Lien vers le fil que vous souhaitez fusionner"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
@@ -2159,19 +2228,19 @@ msgstr ""
 "La fusion supprimera le fil actuel et son contenu sera déplacé vers le fil "
 "spécifié ici."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr "Déplacer le fil"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr "Vous ne pouvez pas déplacer ce fil pour le moment."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr "Le fil a été déplacé."
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr "Nouvelle catégorie"
 
@@ -2209,15 +2278,15 @@ msgstr "Activée"
 msgid "Disabled"
 msgstr "Désactivé"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr "Se désabonner"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr "S'abonner"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr "S'abonner avec e-mail"
 
@@ -2249,11 +2318,11 @@ msgstr "Dernier message"
 msgid "Options"
 msgstr "Options"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr "Ajouter un sondage"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2278,7 +2347,7 @@ msgstr ""
 msgid "Change subscription"
 msgstr "Changer l'abonnement"
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr "Démarrer une discussion"
 
@@ -2358,7 +2427,7 @@ msgstr "Épingler localement "
 msgid "Unpin threads"
 msgstr "Détacher les fils"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr "Déplacer les fils"
 
@@ -2394,7 +2463,7 @@ msgstr "Modération des fils"
 msgid "One or more threads could not be deleted:"
 msgstr "Un ou plusieurs fils n'ont pas pu être supprimés:"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2410,11 +2479,11 @@ msgstr ""
 "Vous devez disposer des droits de création de fils dans cette catégorie afin"
 " de pouvoir les fusionner."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr "Les fils sélectionnés ont été déplacés."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
@@ -2422,51 +2491,51 @@ msgstr ""
 "Vous devez disposer des droits de création de fils dans cette catégorie afin"
 " de pouvoir les déplacer."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr "Tout sélectionner"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr "Tout désélectionner"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr "Tout"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr "Toutes les discussions"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr "Mes fils"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr "Mes fils"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr "Nouveaux fils"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr "Non lu"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr "Fils non lus"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr "Souscrit"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr "Fils souscrits"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr "Contenu non approuvé"
 
@@ -2484,7 +2553,8 @@ msgstr ""
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr "Fils de discussion privés"
 
@@ -2492,68 +2562,68 @@ msgstr "Fils de discussion privés"
 msgid "Are you sure you want to sign out?"
 msgstr "Êtes-vous certain⋅e de vouloir vous déconnecter ?"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr "Voir votre profil"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr "Changer les options"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr "Changer l'avatar"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr "Se déconnecter"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr "%(username)s est banni⋅e jusqu’à %(ban_expires)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr "%(username)s est banni⋅e"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr "%(username)s cache sa présence "
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr "%(username)s est en ligne (caché⋅e)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr ""
 "%(username)s a été vu pour la dernière fois le %(last_click)s (caché⋅e)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr "%(username)s est en ligne"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr "%(username)sa été vu pour la dernière fois le %(last_click)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr "Banni⋅e"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr "En ligne (caché⋅e)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr "Hors ligne (caché⋅e)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr "En ligne"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr "Hors ligne"
 
@@ -2567,19 +2637,19 @@ msgstr[1] "%(followers)s suiveurs"
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr "Aucun utilisateur n'a posté de message depuis %(days)s jours"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr "Rang"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr "Messages classés"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr "Nombre total de messages"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] "%(posters)s a été l'utilisateur le plus actif depuis%(days)sjours"
@@ -2611,7 +2681,7 @@ msgstr "oui"
 msgid "no"
 msgstr "non"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
@@ -2619,19 +2689,19 @@ msgstr ""
 "Les fils privés sont des fils auxquels seuls leurs créateurs et les "
 "personnes invitées peuvent avoir accès et y participer."
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr "Vous ne participez à aucune discussion privée."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr "Connexion à l'application perdue."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr "Le lien n'est pas valide."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr "Une erreur inconnue s'est produite."
 
@@ -2670,15 +2740,23 @@ msgstr "Vous travaillez déjà sur un sondage. Voulez-vous l'abandonner ?"
 msgid "You don't have permission to perform this action."
 msgstr "Vous n'avez pas la permission d'effectuer cette action."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr "Vous êtes banni⋅e"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr "Ce champ est requis."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2692,7 +2770,7 @@ msgstr[1] ""
 "Assurez-vous que cette valeur comporte au moins  %(limit_value)s caractères "
 "(actuellement %(show_value)s)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2706,7 +2784,7 @@ msgstr[1] ""
 "Assurez-vous que cette valeur comporte au plus  %(limit_value)s caractères "
 "(actuellement %(show_value)s)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] ""
@@ -2714,7 +2792,7 @@ msgstr[0] ""
 msgstr[1] ""
 "Le nom d'utilisateur doit comporter au moins %(limit_value)s caractères."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] ""
@@ -2722,13 +2800,13 @@ msgstr[0] ""
 msgstr[1] ""
 "Le nom d'utilisateur peut comporter au plus %(limit_value)s caractères."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr ""
 "Le nom d'utilisateur ne peut contenir que des lettres et des chiffres de "
 "l'alphabet latin."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural ""
 "Valid password must be at least %(limit_value)s characters long."

BIN
misago/locale/ru/LC_MESSAGES/django.mo


+ 728 - 303
misago/locale/ru/LC_MESSAGES/django.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: alff0x1f <alff3one@gmail.com>, 2018\n"
 "Language-Team: Russian (https://www.transifex.com/misago/teams/65369/ru/)\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr "Права доступа"
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr "Роли пользователей"
 
@@ -142,17 +142,17 @@ msgstr "Нужно выбрать один или более элементов.
 msgid "Action is not allowed."
 msgstr "Действие запрещено."
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr "Устаревшая: %(current)s! (свежая: %(latest)s)"
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr "Самая свежая! (%(current)s)"
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr "Не удалось соединиться с GitHub API. Попробуйте позднее."
 
@@ -171,11 +171,11 @@ msgstr "Иерархия категорий"
 msgid "Category roles"
 msgstr "Роли категорий"
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr "Имя"
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr "Описание"
 
@@ -183,7 +183,7 @@ msgstr "Описание"
 msgid "Optional description explaining category intented purpose."
 msgstr "Необязательное описание, объясняющее назначение категории."
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr "CSS класс"
 
@@ -205,7 +205,7 @@ msgstr ""
 "Только пользователи с соответствующими правами могут оставлять сообщения в "
 "закрытых категориях."
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -218,7 +218,7 @@ msgstr ""
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr "Темы"
 
@@ -533,7 +533,7 @@ msgstr "Неправильный запрос на аутентификацию.
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr "Да"
 
@@ -547,7 +547,7 @@ msgstr "Да"
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr "Нет"
 
@@ -651,7 +651,72 @@ msgstr "Значение должно содержать буквы и цифр
 msgid "Value is too long."
 msgstr "Значение слишком длинное."
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr "Тип"
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr "Заголовок"
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr "Правовая информация"
 
@@ -700,7 +765,7 @@ msgstr ""
 msgid "Policy title"
 msgstr "Заголовок политики"
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr "Политика конфиденциальности"
@@ -729,21 +794,85 @@ msgstr ""
 "доступна для форматирования."
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr "Сноска"
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr "Короткое сообщение, отображаемое в нижней части форума."
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr "Нижняя часть форума."
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr "Условия использования"
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr "От новейшего"
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr "От старого"
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -753,15 +882,15 @@ msgstr "%(title)s написал:"
 msgid "Quoted message:"
 msgstr "Цитата:"
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr "Личные данные"
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr "Контакты"
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr "IP адрес"
 
@@ -869,6 +998,98 @@ msgstr "Активация не удалась"
 msgid "Your account can't be activated at this time."
 msgstr "Ваш аккаунт не может быть активирован сейчас."
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr "Базовые настройки"
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr "Редактировать"
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr "Удалить"
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr "Поиск блокировок"
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr "Прикреплённый файл"
@@ -879,12 +1100,8 @@ msgstr "Тема"
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
-"%(filetype)s, %(size)s, загружено %(uploader)s %(uploaded_on)s с "
-"%(uploader_ip)s."
 
 #: templates/misago/admin/attachments/list.html:53
 #: templates/misago/admin/attachmenttypes/list.html:47
@@ -934,11 +1151,6 @@ msgstr "Базовые опции"
 msgid "Availability"
 msgstr "Доступность"
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr "Тип"
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr "Расширения"
@@ -951,19 +1163,6 @@ msgstr "Mime-типы"
 msgid "Files"
 msgstr "Файлы"
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr "Редактировать"
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr "Сейчас нет определённых типов вложения."
@@ -980,12 +1179,12 @@ msgid "New ban"
 msgstr "Новая блокировка"
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr "Настройка блокировки"
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr "Сообщения"
 
@@ -993,8 +1192,8 @@ msgstr "Сообщения"
 msgid "Ban"
 msgstr "Блокировка"
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr "Истечёт в "
 
@@ -1007,10 +1206,6 @@ msgstr "%(check_type)s, только для регистрации"
 msgid "Never"
 msgstr "Никогда"
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr "Удалить"
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr "Запрошенные блокировки по заданным критериям поиска не были найдены"
@@ -1023,10 +1218,6 @@ msgstr "В данный момент нет установленных блок
 msgid "Are you sure you want to remove this ban?"
 msgstr "Вы уверены, что хотите удалить эту блокировку?"
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr "Поиск блокировок"
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr "Администрирование Misago"
@@ -1158,11 +1349,6 @@ msgstr "Вы уверены, что хотите отказаться от из
 msgid "New role"
 msgstr "Новая роль"
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr "Базовые настройки"
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr "Роль категории"
@@ -1199,6 +1385,61 @@ msgstr ""
 msgid "Change settings"
 msgstr "Изменить настройки"
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr "Пользователь"
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr "Статус"
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr "Аватар"
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1355,44 +1596,73 @@ msgstr "Последняя"
 msgid "Administration Home"
 msgstr "Главная панели администрирования"
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr "Версия Misago"
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr "Проверить версию"
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr "Эта функция требует python модуль \"packaging\"."
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr "Содержимое базы данных"
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr "Сообщения"
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr "Пользователи"
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr "Деактивированные пользователи"
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr "Проверка..."
 
@@ -1420,7 +1690,7 @@ msgstr "Пожалуйста попробуйте снова."
 msgid "Username or e-mail"
 msgstr "Имя пользователя или e-mail"
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr "Пароль"
@@ -1465,17 +1735,13 @@ msgid "Display and visibility"
 msgstr "Отображение и видимость"
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
 #: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: users/forms/admin.py:238
 msgid "Rank"
 msgstr "Ранг"
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr "Заголовок"
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr "Специальное"
@@ -1517,7 +1783,7 @@ msgid "No user roles are currently defined."
 msgstr "Сейчас нет определённых ролей пользователя."
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr "Блокировать пользователей"
 
@@ -1525,14 +1791,11 @@ msgstr "Блокировать пользователей"
 msgid "Ban selected users:"
 msgstr "Блокировать выбранных пользователей:"
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
-msgstr "Аватар"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
+msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr "Установить блокировки"
 
@@ -1629,6 +1892,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr "Нет доступного сообщения администраторов."
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1636,51 +1911,55 @@ msgstr "Нет доступного сообщения администрато
 msgid "New user"
 msgstr "Новый пользователь"
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
-msgstr "Пользователь"
-
-#: templates/misago/admin/users/list.html:20
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr "Присоединился"
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr "Деактивирован администратором"
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr "Требует активации администратором"
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr "Должен активировать аккаунт"
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr "Супер администратор"
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr "Администратор"
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr "Редактировать пользователя"
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr "Пользователи по заданным критериям поиска не были найдены."
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr "Искать пользователей"
 
@@ -1898,6 +2177,32 @@ msgstr "Для изменения пароля от аккаунта, кликн
 msgid "Set new password"
 msgstr "Установить новый пароль"
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -2000,11 +2305,9 @@ msgstr ""
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
 msgstr ""
-"%(user)s, вы получили это сообщение, потому что %(poster)s ответил в теме "
-"%(thread)s, на которую вы подписаны."
 
 #: templates/misago/emails/thread/reply.html:14
 #: templates/misago/emails/thread/reply.txt:10
@@ -2018,11 +2321,9 @@ msgstr "Перейти к ответу"
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
 msgstr ""
-"%(user)s, вы получили это сообщение, потому что %(poster)s ответил в теме "
-"\"%(thread)s\" , на которую вы подписаны."
 
 #: templates/misago/errorpages/403.html:40
 msgid "This page is not available."
@@ -2280,7 +2581,7 @@ msgstr "Смотреть результаты"
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr "Детали блокировки"
 
@@ -2331,7 +2632,7 @@ msgstr[3] "Начал %(threads)s тем."
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr "Детали"
 
@@ -2360,7 +2661,7 @@ msgstr ""
 "Эта ошибка случилось из-за неправильной манипуляции содержимым сообщения."
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr "Последователи"
 
@@ -2392,7 +2693,7 @@ msgid "%(username)s has no followers."
 msgstr "У %(username)s нет последователей."
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr "Следить"
 
@@ -2491,7 +2792,7 @@ msgid "%(username)s started no threads."
 msgstr "У %(username)s нет начатых тем."
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr "История имени пользователя"
 
@@ -2522,6 +2823,20 @@ msgstr "Ваше имя пользователя никогда не измен
 msgid "%(username)s's username was never changed."
 msgstr "Имя пользователя %(username)s никогда не изменялось."
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr "Поиск по сайту"
@@ -2640,10 +2955,6 @@ msgstr "Скрыто %(hidden_by)s %(hidden_on)s."
 msgid "By %(event_by)s on %(event_on)s."
 msgstr "%(event_by)s %(event_on)s."
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr "IP записан"
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2940,11 +3251,11 @@ msgstr "Вложения"
 msgid "Attachment types"
 msgstr "Типы вложений"
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr "У вас нет разрешения загружать новые файлы."
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr "Нет обновлённых файлов."
 
@@ -2952,11 +3263,11 @@ msgstr "Нет обновлённых файлов."
 msgid "Uploaded image was corrupted or invalid."
 msgstr "Обновлённое изображение испорчено или неправильно."
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr "Вы не можете загрузить файлы этого типа."
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
@@ -2964,7 +3275,7 @@ msgstr ""
 "Вы не можете загрузить файлы больше чем %(limit)s (У вашего файл есть "
 "%(upload)s)"
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
@@ -2973,7 +3284,7 @@ msgstr ""
 "Вы не можете загрузить файлы этого типа, больше чем %(limit)s (Ваш файл "
 "имеет %(upload)s)."
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr "Правки записи недоступны для этой публикации."
 
@@ -2989,12 +3300,12 @@ msgstr "Вы не можете перемещать публикации в эт
 msgid "You can't like posts in this category."
 msgstr "Вам не может нравится публикации в этой категории."
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr "Одобрение содержимого не может быть отменено."
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr "Одна или больше публикаций для обновления, не могут быть найдены."
 
@@ -3007,7 +3318,7 @@ msgstr "Вы не можете разделить публикации в это
 msgid "You don't have permission to remove \"%(attachment)s\" attachment."
 msgstr "У вас нет разрешения удалять вложение \"%(attachment)s\"."
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -3102,11 +3413,11 @@ msgstr[3] ""
 msgid "One or more users could not be found: %(usernames)s"
 msgstr "Один или более пользователей не найдены: %(usernames)s"
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr "Вы должны ввести сообщение."
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr "Вы должны ввести заголовок темы."
 
@@ -3188,7 +3499,7 @@ msgstr "Этот пользователь уже владелец темы."
 msgid "One or more threads to update could not be found."
 msgstr "Одна или больше тем для обновления не могут быть найдены."
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr "В этой теме уже есть опрос."
 
@@ -3224,7 +3535,7 @@ msgstr "Имя файла содержит"
 msgid "File type"
 msgstr "Тип файла"
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr "Состояние"
 
@@ -3248,10 +3559,6 @@ msgstr "Расширения файла"
 msgid "Maximum allowed uploaded file size"
 msgstr "Максимально позволенная загрузка размера файла"
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr "Статус"
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr "Ограничение загрузок в"
@@ -3907,7 +4214,7 @@ msgstr ""
 msgid "Can see threads"
 msgstr "Можно смотреть темы"
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4884,6 +5191,14 @@ msgstr "Один или больше выборов опроса неправи
 msgid "You have to make a choice."
 msgstr "Вы должны сделать выбор."
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -5065,23 +5380,13 @@ msgstr "У вас нет разрешения видеть не одобренн
 msgid "Requested attachment could not be found."
 msgstr "Запрошенное вложение не может быть найдено."
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr "От новейшего"
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr "От старого"
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr "От А до я"
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr "От Я до а"
 
@@ -5154,33 +5459,37 @@ msgstr ""
 "Вам нужно разрешение одобрять содержимое, чтобы быть способным перейти к "
 "первой не одобренной публикации."
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr "Аккаунты пользователя"
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr "Ранги"
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr "Запреты"
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr "Активировать аккаунт %(user)s на %(forum_name)s форумах."
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr "Изменить пароль %(user)s на %(forum_name)s форумах"
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr "Ссылка формы неправильна. Пожалуйста, попробуйте заново."
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr "Ваша ссылка истекла. Пожалуйста, запросите новую."
 
@@ -5250,15 +5559,15 @@ msgstr "Ссылка подтверждения изменения e-mail-а б
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr "Подтвердите изменение пароля на %(forum_name)s форумах."
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr "Ссылка подтверждения изменения пароля было выслано на новый адрес."
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr "Новые регистрации пользователи сейчас закрыты."
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr "Пожалуйста, попробуйте повторно отправить эту форму."
 
@@ -5283,38 +5592,54 @@ msgstr "Ошибка изменения имени пользователя. П
 msgid "You don't have permission to see other users name history."
 msgstr "У вас нет разрешения просматривать историю имён других пользователей."
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr "Вы должны войти, чтобы выполнить это действие."
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr "Вы не можете изменять аватары других пользователей."
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr "Вы не можете изменять настройки других пользователей."
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr "Ваши настройки форума были изменены."
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr "Вы не можете менять имена других пользователей."
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr "Вы не можете менять подписи других пользователей."
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr "Вы не можете менять пароли других пользователей."
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr "Вы не можете менять e-mail адреса других пользователей."
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr "Редактировать детали"
@@ -5327,11 +5652,11 @@ msgstr "Изменить имя пользователя"
 msgid "Change email or password"
 msgstr "Изменить email или пароль"
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -5390,27 +5715,27 @@ msgstr "Редактировать разрешения и группы"
 msgid "Edit the user from Misago admin panel"
 msgstr "Редактировать пользователя из панели администрирования Misago"
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr "Имя пользователя"
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr "Пользовательский заголовок"
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr "E-mail адрес"
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr "Все зарегистрированные участники должны иметь роль — \"Участник\"."
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr "Является администратором"
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to"
@@ -5421,11 +5746,11 @@ msgstr ""
 "дополнительные разрешения назначенные внутри, чтобы администрировать модули "
 "Django."
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr "Является суперпользователем"
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
@@ -5434,11 +5759,11 @@ msgstr ""
 "дополнение доступу к административному сайту, супер администратор также "
 "может менять административные уровни других участников."
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr "Является активным"
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
@@ -5446,11 +5771,11 @@ msgstr ""
 "Определяет, должен ли пользователь считаться активным. Выключить это — "
 "означает удалить аккаунты пользователя неразрушимым способом."
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr "Сообщение персонала"
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
@@ -5458,15 +5783,15 @@ msgstr ""
 "Дополнительное сообщение для участников команды форума, объясняющее почему "
 "аккаунт пользователя был отключён."
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr "Изменить пароль на"
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr "Блокировка аватара"
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
@@ -5474,12 +5799,12 @@ msgstr ""
 "Настройте это как \"да\", чтобы остановить пользователя изменять его/её "
 "аватар, и сбросить его/её аватар для процедурной генерации."
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr "Сообщение пользователя"
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
@@ -5487,7 +5812,7 @@ msgstr ""
 "Дополнительное сообщение для пользователя, объясняющее почему ему запрещено "
 "менять аватар."
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
@@ -5495,47 +5820,47 @@ msgstr ""
 "Дополнительное сообщение для членов команды форума, объясняющее почему "
 "пользователь был заблокирован за форму изменения аватара."
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr "Подпись содержимого"
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr "Блокировка подписи"
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr ""
 "Настройте это как \"да\", чтобы остановить пользователя изменять его /её "
 "подпись."
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr ""
 "Дополнительное сообщение для пользователя, объясняющее почему его/ее подпись"
 " заблокирована."
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr ""
 "Дополнительное сообщение для участников команды, объясняющее почему подпись "
 "пользователя заблокирована."
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr "Скрывает присутствие"
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr "Кто может добавлять пользователей в приватные темы."
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr "Отвеченные темы"
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
@@ -5544,7 +5869,7 @@ msgstr[1] "Подпись не может быть длиннее %(limit)s си
 msgstr[2] "Подпись не может быть длиннее %(limit)s символов."
 msgstr[3] "Подпись не может быть длиннее %(limit)s символов."
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
@@ -5552,61 +5877,61 @@ msgstr ""
 "Ранги используются для группировки и разпознавания пользователей. Они также "
 "используются для добавления разрешений для групп пользователей."
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr "Роли"
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr ""
 "Индивидуальная роль для этого пользователя. Все пользователи должны иметь "
 "роли \"участников\"."
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr "Имя пользователя начинается с"
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr "E-mail начинается с"
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr "Поля профиля содержат"
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr "Только неактивные"
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr "Только деактивированные"
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr "Только администраторы"
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr "Все ранги"
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr "Все роли"
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr "Имеет ранг"
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr "Имеет роль"
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
@@ -5614,11 +5939,11 @@ msgstr ""
 "Короткое и наглядное имя пользователей с этим рангом. \"Команда\" или "
 "\"Мастера\" являются хорошим примером."
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr "Пользовательский заголовок"
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example"
 " \"GM\" or \"Dev\"."
@@ -5626,7 +5951,7 @@ msgstr ""
 "Дополнительная, единичная версия имени ранга, которая показывается именами "
 "пользователей. Например, \"GM\" или \"Dev\"."
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
@@ -5634,21 +5959,21 @@ msgstr ""
 "Необязательное описание, объясняющее функцию или статус членов, связанных с "
 "этим рангом."
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr "Ранг может давать дополнительные роли пользователям."
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr ""
 "Дополнительный класс CSS, добавленный к контенту, принадлежащий владельцу "
 "этого ранга."
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr "Дайте вкладку  рангу в списке пользователей"
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
@@ -5656,70 +5981,70 @@ msgstr ""
 "Выбор данной опции сделает пользователей данного ранга легко распознаваемыми"
 " для других через соответствующий список пользователей на странице форума."
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr "Это имя совпадает с именем другого ранга."
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr "Значения для блокировки"
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
-msgid "Usernames"
-msgstr "Имена пользователя"
-
-#: users/forms/admin.py:465 users/forms/admin.py:580
-msgid "E-mails"
-msgstr "E-mail'ы"
-
-#: users/forms/admin.py:466
-msgid "E-mail domains"
-msgstr "E-mail домены"
-
-#: users/forms/admin.py:467
-msgid "IP addresses"
-msgstr "IP адреса"
-
-#: users/forms/admin.py:468
-msgid "First segment of IP addresses"
-msgstr "Первый сегмент IP адресов"
-
-#: users/forms/admin.py:469
-msgid "First two segments of IP addresses"
-msgstr "Первые два сегмента IP адресов"
-
-#: users/forms/admin.py:476
+#: users/forms/admin.py:471
 msgid "Optional message displayed to users instead of default one."
 msgstr ""
 "Не обязательное сообщение, отображаемое пользователям вместо стандартного."
 
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
 msgid "Message can't be longer than 1000 characters."
 msgstr "Сообщение не может быть длиннее 1000 символов."
 
-#: users/forms/admin.py:483 users/forms/admin.py:536
+#: users/forms/admin.py:478 users/forms/admin.py:550
 msgid "Team message"
 msgstr "Сообщение команды"
 
-#: users/forms/admin.py:486 users/forms/admin.py:539
+#: users/forms/admin.py:481 users/forms/admin.py:553
 msgid "Optional ban message for moderators and administrators."
 msgstr ""
 "Не обязательное сообщение при блокировке для модераторов и администраторов."
 
-#: users/forms/admin.py:495
+#: users/forms/admin.py:490
 msgid "Leave this field empty for set bans to never expire."
 msgstr "Оставьте это поле пустым, чтобы блокировки никогда не заканчивались."
 
+#: users/forms/admin.py:499 users/forms/admin.py:593
+msgid "Usernames"
+msgstr "Имена пользователя"
+
+#: users/forms/admin.py:500 users/forms/admin.py:594
+msgid "E-mails"
+msgstr "E-mail'ы"
+
 #: users/forms/admin.py:501
+msgid "E-mail domains"
+msgstr "E-mail домены"
+
+#: users/forms/admin.py:507
+msgid "IP addresses"
+msgstr "IP адреса"
+
+#: users/forms/admin.py:508
+msgid "First segment of IP addresses"
+msgstr "Первый сегмент IP адресов"
+
+#: users/forms/admin.py:509
+msgid "First two segments of IP addresses"
+msgstr "Первые два сегмента IP адресов"
+
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr "Проверить тип"
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr "Ограничить действие этой блокировки только на регистрацию."
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5731,11 +6056,11 @@ msgstr ""
 "определенные регистрации, отклоненные недавно Email провайдерами, без ущерба"
 " существующих пользователей."
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr "Заблокированное значение"
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
@@ -5745,51 +6070,74 @@ msgstr ""
 "совпадения. К примеру, установив блокировку по IP со значением \"83.*\", "
 "будут заблокированы все IP адреса, начинающиеся с \"83.\"."
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr "Заблокированное значение может быть не длиннее 250 символов."
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr ""
 "Не обязательное сообщение, отображаемое пользователю вместо стандартного."
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr "Оставьте это поле пустым, чтобы блокировка никогда не заканчивалась."
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr "Заблокированное значение слишком расплывчато."
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr "Все блокировки"
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr "IP"
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr "Заблокированное значение начинается с"
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr "Только регистрация"
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr "Все"
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr "Активные"
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr "Истекшие"
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case"
+" insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr "Заполнить оба поля."
@@ -5850,18 +6198,27 @@ msgstr ""
 "Администратор должен активировать Ваш аккаунт до того как вы сможете "
 "запросить новый пароль."
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr "Это имя пользователя недоступно."
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr "Этот e-mail адрес недоступен."
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr "Новые регистрации с данного IP адреса недоступны."
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -6096,7 +6453,7 @@ msgid "Enter each answer in new line. Answers are case-insensitive."
 msgstr "Введите каждый ответ в новой строке. Ответы чуствительны к регистру."
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr "Форум команды"
 
@@ -6109,47 +6466,59 @@ msgstr "Команда"
 msgid "Members"
 msgstr "Участники"
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr "Пользователь обязан иметь e-mail адрес."
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr "Уведомлять"
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr "Уведомлять по e-mail"
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr "Все"
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr "Пользователи, которых я читаю"
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr "Никто"
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr "присоединился к"
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr "статус сотрудника"
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr "Указывает, может ли пользователь заходить на администраторские сайты."
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr "активен"
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6541,11 +6910,7 @@ msgstr "Это неправильный twitter аккаунт."
 msgid "Join IP"
 msgstr "Подсоединить IP-адрес"
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr "Последний IP"
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr "Добро пожаловать на %(forum_name)s форумы!"
@@ -6587,14 +6952,30 @@ msgstr "Вам необходимо ввести новый e-mail адрес."
 msgid "New e-mail is same as current one."
 msgstr "Новый e-mail совпадает с текущим."
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available"
 " for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6704,6 +7085,42 @@ msgstr "Блокировка \"%(name)s\" была отредактирован
 msgid "Ban \"%(name)s\" has been removed."
 msgstr "Блокировка \"%(name)s\" была удалена."
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr "Запрашиваемый ранг не существует."
@@ -6753,39 +7170,43 @@ msgstr "Ранг \"%(name)s\" уже является рангом по умол
 msgid "Rank \"%(name)s\" has been made default."
 msgstr "Ранг \"%(name)s\" был назначен рангом по умолчанию."
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr "Наибольшие постеры"
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr "Наименьшие постеры"
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr "С пользователями: 0"
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr "Выбрать пользователей"
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr "Активировать аккаунты"
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr "Удалить аккаунты"
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr "Вы уверены что хотите удалить выбранных пользователей?"
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr "Удалить всё"
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
@@ -6793,54 +7214,58 @@ msgstr ""
 "Вы уверены что хотите удалить выбранных пользователей? Это также удалит все "
 "данные, связанные с этими аккаунтами."
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr "Вы должны выбрать неактивных пользователей."
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr "Ваш аккаунт на форуме %(forum_name)s был активирован"
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr "Выбранные аккаунты пользователей были активированы."
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr "%(user)s - суперадминистратор и не может быть заблокирован."
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr "Выбранные пользователи были заблокированы."
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr "Вы не можете удалить себя."
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr "%(user)s - администратор и не может быть удален."
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr "Выбранные пользователи были удалены."
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr "Новый пользователь \"%(user)s\" был зарегистрирован."
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr "Пользователь \"%(user)s\" был отредактирован."
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr "Это действие нельзя выполнить напрямую."
 

BIN
misago/locale/ru/LC_MESSAGES/djangojs.mo


+ 285 - 207
misago/locale/ru/LC_MESSAGES/djangojs.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-20 21:20+0200\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Roman Patsev <rpatsev@gmail.com>, 2018\n"
 "Language-Team: Russian (https://www.transifex.com/misago/teams/65369/ru/)\n"
@@ -39,8 +39,30 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
-msgstr "Регистрируясь, вы соглашаетесь с условиями и положениями сайта"
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
+msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
@@ -48,11 +70,12 @@ msgstr "Регистрируясь, вы соглашаетесь с услов
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr "Закрыть"
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr "Добавить участника"
 
@@ -70,9 +93,10 @@ msgstr "Добавление пользователя"
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr "Отмена"
 
@@ -199,8 +223,8 @@ msgid "Generate my individual avatar"
 msgstr "Сгенерировать индивидуальную аватарку"
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr "Ок"
 
@@ -268,39 +292,39 @@ msgstr "Вставить код"
 msgid "Emphase selection"
 msgstr "Акцентировать выделенное"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr "Вставить горизонтальный разделитель"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr "Введите ссылку на изображение"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr "Введите подпись изображения (опционально)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr "Вставить изображение"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr "Введите адрес ссылки"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr "Введите подпись ссылки (опционально)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr "Вставить ссылку"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr "Введите автора цитаты, ник пишите с префиксом @"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr "Вставить цитату"
 
@@ -332,7 +356,7 @@ msgstr "Отменить удаление"
 msgid "Error uploading %(filename)s"
 msgstr "Ошибка загрузки  %(filename)s"
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr "Отклонено"
 
@@ -349,7 +373,7 @@ msgid "Protected"
 msgstr "Защищено"
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr "Защитить"
 
@@ -384,7 +408,7 @@ msgid ""
 " deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr "Опрос"
 
@@ -399,6 +423,7 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr "Объединить темы"
 
@@ -433,8 +458,7 @@ msgstr "Опубликовано %(poster)s%(posted_on)sв категории \"
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr "%(title)s, присоединился %(joined_on)s"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr "Изменить имя"
 
@@ -471,7 +495,7 @@ msgstr[3] ""
 msgid "Your new username is same as current one."
 msgstr "Вашено новое имя совпадает с текущим."
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr "Новое имя"
 
@@ -480,15 +504,15 @@ msgid "Your username has been changed successfully."
 msgstr "Имя было успешно изменено."
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr "Измените ваши настройки"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr "Удалить аккаунт"
 
@@ -524,7 +548,54 @@ msgstr ""
 msgid "Delete my account"
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr "Личные данные обновлены."
 
@@ -609,7 +680,7 @@ msgstr "Темы, которые я начал"
 msgid "Threads I reply to"
 msgstr "Темы, где я отвечал"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr "Изменить email или пароль"
 
@@ -659,7 +730,7 @@ msgstr "Новый пароль"
 msgid "Repeat password"
 msgstr "Повторите пароль"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr "Изменить забытый пароль"
 
@@ -851,11 +922,11 @@ msgstr[1] "%(votes)s голоса."
 msgstr[2] "%(votes)s голосов."
 msgstr[3] "%(votes)s голосов."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr "Голоса публичны."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] "%(votes)s голос, %(proc)s%"
@@ -863,11 +934,11 @@ msgstr[1] "%(votes)s голоса, %(proc)s%"
 msgstr[2] "%(votes)s голосов, %(proc)s%"
 msgstr[3] "%(votes)s голосов, %(proc)s%"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr "Ваш выбор."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] "%(votes)s пользователь проголосовал за этот вариант"
@@ -887,9 +958,8 @@ msgstr "Ответ"
 msgid "See votes"
 msgstr "Посмотрть ответы"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr "Изменить"
 
@@ -901,8 +971,7 @@ msgstr ""
 "необратимо."
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr "Удалить"
 
@@ -969,28 +1038,28 @@ msgstr "Сообщение было возвращено в предыдущее
 msgid "See previous change"
 msgstr "Посмотреть предыдущее изменение"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr "Опубликовал  %(poster)s  %(posted_on)s."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr "Содержимое данного сообщения не может быть отображено."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr ""
 "Эта ошибка произошла из-за неправильной манипуляции содержимым сообщения."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr "Опубликовано  %(posted_on)s"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr "Удалённый пользователь"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr "Просмотреть сообщение"
 
@@ -1014,7 +1083,7 @@ msgstr "Ни одному пользователю ещё не понравил
 msgid "Are you sure you want to discard changes?"
 msgstr "Вы уверены, что хотите сбросить изменения?"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr "Необходимо ввести сообщение."
 
@@ -1046,11 +1115,12 @@ msgstr "Вы уверены, что хотите сбросить приватн
 msgid "You have to enter at least one recipient."
 msgstr "Необходимо указать хотя бы одного получателя."
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr "Вы должны ввести заголовок темы."
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr "Ваша тема была опубликована."
 
@@ -1058,12 +1128,13 @@ msgstr "Ваша тема была опубликована."
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr "Список пользователей через запятую, например: Иван, Василий"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr "Заголовок темы"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr "Опубликовать тему"
 
@@ -1071,35 +1142,35 @@ msgstr "Опубликовать тему"
 msgid "Are you sure you want to discard thread?"
 msgstr "Вы уверены что хотите удалить ветку?"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr "Закрыто"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr "Открыто"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr "Скрытые"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr "Не скрывать"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr "Откреплено"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr "Закреплено локально"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1186,12 +1257,12 @@ msgstr[3] ""
 "%(show_value)s)."
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr "Спрятать"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr "Показать"
 
@@ -1213,10 +1284,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr "%(event_by)s %(event_on)s."
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr "IP зарегистрированный"
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr "Заголовок ветки был изменён с %(old_title)s."
 
@@ -1306,23 +1373,23 @@ msgstr ""
 msgid "Post has been deleted."
 msgstr "Сообщение удалено."
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr "Постоянная ссылка на это сообщение:"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr "Постоянная ссылка"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] "Это сообщение было отредактировано %(edits)s раз."
@@ -1334,17 +1401,15 @@ msgstr[3] "Это сообщение было отредактировано %(e
 msgid "Changes history"
 msgstr "История изменений"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr "Одобрить"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr "Переместить"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr "Разделить"
 
@@ -1357,6 +1422,7 @@ msgid "Move post"
 msgstr "Переместить сообщение"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr "Вы должны указать ссылку на другую тему."
 
@@ -1406,7 +1472,7 @@ msgid "Close thread"
 msgstr "Закрыть тему"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr "Категория"
 
@@ -1460,32 +1526,32 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr "Нравится"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr "Лайк"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr "Ответить"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr "Новое сообщение"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr "Новые"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr "Это сообщение защищено и не может быть отредактировано."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr "защищено"
 
@@ -1537,23 +1603,23 @@ msgstr "Вы не поделились информацией о себе с д
 msgid "%(username)s is not sharing any details with others."
 msgstr "%(username)s не поделился никакими деталями с другими."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr "Детали"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr "%(username)sличные данные обновлены."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr "У вас нет начатых тем."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr "У %(username)s нет начатых тем."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] "Вы начали %(threads)sтему."
@@ -1561,7 +1627,7 @@ msgstr[1] "Вы начали %(threads)sтемы."
 msgstr[2] "Вы начали %(threads)sтем."
 msgstr[3] "Вы начали %(threads)s тем."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] ""
@@ -1569,25 +1635,24 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr "Загрузка..."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr "Темы"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr "Вы не написали сообщений."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr "У %(username)s нет опубликованных сообщений."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] "Вы опубликовали %(posts)s сообщение."
@@ -1595,7 +1660,7 @@ msgstr[1] "Вы опубликовали %(posts)s сообщения."
 msgstr[2] "Вы опубликовали %(posts)s сообщений."
 msgstr[3] "Вы опубликовали %(posts)s сообщений."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] ""
@@ -1603,11 +1668,11 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr "Сообщений"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr "Показать ещё (%(more)s)"
 
@@ -1708,6 +1773,7 @@ msgid "Joined %(joined_on)s"
 msgstr "Присоединился %(joined_on)s"
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr "Модерация"
 
@@ -1763,41 +1829,41 @@ msgstr ""
 msgid "Avatar controls"
 msgstr "Контроль за аватаром"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr "Имя пользователя было изменено."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr ""
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr "Пользовательский контент"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr "Удалить с аккаунтом пользователя"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr "Скрыть после удаления аккаунта пользователя."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr "Вернуться к списку пользователей"
 
@@ -1869,75 +1935,74 @@ msgstr "Регистрация недоступна из-за ошибки."
 msgid "Register"
 msgstr "Регистрация"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr "Имя пользователя"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr "Электронная почта"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr "Пароль"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr "Зарегистрировать аккаунт"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr "Регистрация завершена"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr "Введите корректный e-mail адрес."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr "Ваш e-mail адрес"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr "Отправить ссылку"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr "Запросить другую ссылку"
 
@@ -2002,41 +2067,41 @@ msgstr "Пользователи по заданным критериям пои
 msgid "Enter at least two characters to search users."
 msgstr "Введите хотя бы два символа для поиска пользователя."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr "Заполните оба поля."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr "Активировать аккаунт"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr "Имя пользователя или e-mail"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr "Забыли пароль?"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to "
+"%(username)s, your account has been created and you have been signed in to "
 "it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -2045,6 +2110,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -2065,7 +2134,7 @@ msgid "Edit title"
 msgstr "Изменить название"
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr "Одобренные"
 
@@ -2073,7 +2142,7 @@ msgstr "Одобренные"
 msgid "Unapproved posts"
 msgstr "Одобренные темы"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] "%(replies)s ответ"
@@ -2099,19 +2168,19 @@ msgid ""
 msgstr ""
 "Вы уверены, что хотите удалить выбранные сообщения? Это действие необратимо!"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr "Объединить"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr "Снять защиту"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr ""
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr "Одно или больше сообщений не могут быть изменены:"
 
@@ -2171,37 +2240,37 @@ msgstr "Закрепить локально"
 msgid "Unpin"
 msgstr "Открепить"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr "Объединить тему"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr "Тема была объедена с другой."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr "Ссылка на тему, с которой Вы хотите объединить"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
 msgstr ""
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr "Переместить тему"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr "Вы не можете переместить тему на данный момент."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr "Тема была перемещена."
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr "Новая категория"
 
@@ -2241,15 +2310,15 @@ msgstr "Активна"
 msgid "Disabled"
 msgstr "Неактивна"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr "Отписаться "
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr "Подписаться"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr "Уведомлять по e-mail"
 
@@ -2281,11 +2350,11 @@ msgstr "Последнее сообщение"
 msgid "Options"
 msgstr "Настройки"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr "Добавить опрос"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2308,7 +2377,7 @@ msgstr ""
 msgid "Change subscription"
 msgstr "Изменить подписку"
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr "Новая тема"
 
@@ -2388,7 +2457,7 @@ msgstr "Прикрепить темы локально"
 msgid "Unpin threads"
 msgstr "Открепить темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr "Переместить темы"
 
@@ -2424,7 +2493,7 @@ msgstr "Модерация тем"
 msgid "One or more threads could not be deleted:"
 msgstr "Одна или более тем не могут быть удалены:"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2436,61 +2505,61 @@ msgid ""
 " to it."
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr "Выбранные темы были перенесены."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
 msgstr ""
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr "Выбрать всё"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr "Ничего не выбрано"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr "Все"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr "Все темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr "Мои"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr "Мои темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr "Новые темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr "Непрочитанные"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr "Непрочитанные темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr "Подписки"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr "Подписаться на темы"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr "Не одобренное содержимое"
 
@@ -2506,7 +2575,8 @@ msgstr "Войдите или зарегистрируйтесь, чтобы у
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr "Приватные темы"
 
@@ -2514,67 +2584,67 @@ msgstr "Приватные темы"
 msgid "Are you sure you want to sign out?"
 msgstr "Вы уверены что хотите выйти?"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr "Просмотреть профиль"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr "Изменить настройки"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr "Изменить аватар"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr "Выход"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr "%(username)sзаблокирован до %(ban_expires)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr "%(username)sзаблокирован"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr ""
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr "%(username)sв сети (скрыто)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr "%(username)sбыл в сети %(last_click)s(скрыто)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr "%(username)sв сети"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr "%(username)sбыл %(last_click)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr "Забанен"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr "В сети (скрыто)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr "Не в сети (скрыто)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr "В сети"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr "Не в сети"
 
@@ -2590,19 +2660,19 @@ msgstr[3] "%(followers)sпоследователей"
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr ""
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr "Ранг"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr "Рейтинг сообщений"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr "Всего сообщений"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] ""
@@ -2638,25 +2708,25 @@ msgstr "да"
 msgid "no"
 msgstr "нет"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
 msgstr ""
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr "Вы не участвуете ни в каких приватных темах."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr "Потеряно соединение с приложением."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr ""
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr "Произошла неизвестная ошибка."
 
@@ -2694,15 +2764,23 @@ msgstr ""
 msgid "You don't have permission to perform this action."
 msgstr "У Вас нет прав для совершения этого действия."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr "Вы заблокированы"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr "Это поле обязательно."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2714,7 +2792,7 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2726,7 +2804,7 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] ""
@@ -2734,7 +2812,7 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] ""
@@ -2742,11 +2820,11 @@ msgstr[1] ""
 msgstr[2] ""
 msgstr[3] ""
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr "Имя пользователя может содержать только латинские буквы и цифры."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural ""
 "Valid password must be at least %(limit_value)s characters long."

BIN
misago/locale/tr/LC_MESSAGES/django.mo


+ 726 - 303
misago/locale/tr/LC_MESSAGES/django.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Yiğitcan Uçan <ucanyiit@gmail.com>, 2017\n"
 "Language-Team: Turkish (Turkey) (https://www.transifex.com/misago/teams/65369/tr_TR/)\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr "İzinler"
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr "Kullanıcı rolleri"
 
@@ -142,17 +142,17 @@ msgstr "Bir veya daha fazla öğe seçmeniz gerekiyor."
 msgid "Action is not allowed."
 msgstr "İşlem yapılmasına izin verilmiyor."
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr "Güncel olmayan:%(current)s! (en son:%(latest)s)"
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr "Güncel! (%(current)s)"
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr "GitHub API'ye bağlanti hatasi.Lütfen tekrar deneyin"
 
@@ -171,11 +171,11 @@ msgstr "Kategori sıralaması"
 msgid "Category roles"
 msgstr "bölüm rolleri"
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr "İsim"
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr "Açıklama"
 
@@ -183,7 +183,7 @@ msgstr "Açıklama"
 msgid "Optional description explaining category intented purpose."
 msgstr "bölüm beyan edilen isteğe bağlı açıklama."
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr "CSS sınıfı"
 
@@ -201,7 +201,7 @@ msgstr "Kapalı bölüm"
 msgid "Only members with valid permissions can post in closed categories."
 msgstr "Sadece geçerli izni olan üyeler kapalı kategorilerde ileti yapabilir."
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -214,7 +214,7 @@ msgstr "Sadece geçerli izni olan üyeler kapalı kategorilerde ileti yapabilir.
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr "Konular"
 
@@ -524,7 +524,7 @@ msgstr "Kimlik doğrulama geçersiz."
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr "Evet"
 
@@ -538,7 +538,7 @@ msgstr "Evet"
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr "Hayır"
 
@@ -641,7 +641,72 @@ msgstr "Veri alfanümerik karakterler içermelidir."
 msgid "Value is too long."
 msgstr "Veri çok uzun."
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr "Tür"
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr "Başlık"
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr "Yasal bilgi"
 
@@ -689,7 +754,7 @@ msgstr ""
 msgid "Policy title"
 msgstr "ilke başlığı"
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr "Gizlilik ilke"
@@ -716,21 +781,85 @@ msgstr ""
 "biçimlendirme için kullanılabilir."
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr "Dipnote"
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr "Forum alt başlığında kısa ileti görüldü."
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr "Forum altbaşlığı"
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr "Servis koşullari"
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr "En yeni gelen"
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr "Eskilerden"
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -740,15 +869,15 @@ msgstr "%(title)syazıldı:"
 msgid "Quoted message:"
 msgstr "Alıntı ileti:"
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr "Personal"
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr "Temas kur"
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr "IP adres"
 
@@ -856,6 +985,98 @@ msgstr "Aktivasyon başarısız oldu"
 msgid "Your account can't be activated at this time."
 msgstr "Hesabın şuan aktive edilemiyor."
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr "Temel ayarlar"
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr "Düzenle"
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr "Kaldır"
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr "Banlari ara"
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr "Ek"
@@ -866,12 +1087,8 @@ msgstr "konu"
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
-"%(filetype)s,%(size)s,%(uploader)s tarafından "
-"yüklendi%(uploaded_on)sitibaren%(uploader_ip)s"
 
 #: templates/misago/admin/attachments/list.html:53
 #: templates/misago/admin/attachmenttypes/list.html:47
@@ -921,11 +1138,6 @@ msgstr "Temel seçenekler"
 msgid "Availability"
 msgstr "Geçerlilik"
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr "Tür"
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr "Uzatma"
@@ -938,19 +1150,6 @@ msgstr "Mime türleri"
 msgid "Files"
 msgstr "Dosya"
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr "Düzenle"
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr "Hiç bir ek tür tanimlanamadi."
@@ -967,12 +1166,12 @@ msgid "New ban"
 msgstr "Yeni Yasaklamak"
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr "Yasaklamak ayarları"
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr "Mesajlar"
 
@@ -980,8 +1179,8 @@ msgstr "Mesajlar"
 msgid "Ban"
 msgstr "Yasaklamak"
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr "Bitiş süresi"
 
@@ -994,10 +1193,6 @@ msgstr "%(check_type)s,yanlızca kayıt"
 msgid "Never"
 msgstr "Asla"
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr "Kaldır"
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr "Arama kıriterlerine uyuşan hiç bir ban bulunamadı"
@@ -1010,10 +1205,6 @@ msgstr "Şuan hiç bir ban uygulanamadi."
 msgid "Are you sure you want to remove this ban?"
 msgstr "Bu Yasaklamak silmek istediğine emin misiniz?"
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr "Banlari ara"
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr "Misago Yönetimi"
@@ -1145,11 +1336,6 @@ msgstr "Değişikliklerden vazgeçmek istiyor musun?"
 msgid "New role"
 msgstr "Yeni sıfat"
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr "Temel ayarlar"
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr "bölüm sıfat"
@@ -1186,6 +1372,61 @@ msgstr ""
 msgid "Change settings"
 msgstr "Ayarları değiştir"
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr "Kullanici"
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr "durum"
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr "Avatar"
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1340,44 +1581,73 @@ msgstr "Son"
 msgid "Administration Home"
 msgstr "Yönetici evi"
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr "Misago sürümü"
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr "Sürümü kontrol et"
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr "Bu özellik \"paketleme\" python modülünü gerektirir."
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr "DB içerikleri"
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr "iletiler"
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr "Kullanicilar"
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr "Aktif olmayan kullanicilar"
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr "Kontrol ediliyor..."
 
@@ -1405,7 +1675,7 @@ msgstr "Lütfen sonra tekrar deneyin"
 msgid "Username or e-mail"
 msgstr "Kullanıcı adi ve ya e-mail"
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr "Şifre"
@@ -1450,17 +1720,13 @@ msgid "Display and visibility"
 msgstr "Görüntülenme ve görünürlük"
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
 #: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: users/forms/admin.py:238
 msgid "Rank"
 msgstr "Rütbe"
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr "Başlık"
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr "Özel"
@@ -1502,7 +1768,7 @@ msgid "No user roles are currently defined."
 msgstr "Hiç bir kullanici rolü tanimlanamadi."
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr "Yasaklamak  kullanicilar"
 
@@ -1510,14 +1776,11 @@ msgstr "Yasaklamak  kullanicilar"
 msgid "Ban selected users:"
 msgstr "Seçilmiş kullanicilari Yasaklamak :"
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
-msgstr "Avatar"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
+msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr "Banlari kur"
 
@@ -1613,6 +1876,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr "Hiç bir personel ileti mevcut değil."
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1620,51 +1895,55 @@ msgstr "Hiç bir personel ileti mevcut değil."
 msgid "New user"
 msgstr "Yeni kullanici"
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
-msgstr "Kullanici"
-
-#: templates/misago/admin/users/list.html:20
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr "Katildi"
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr "Admin tarafından devre dışı bırakıldı"
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr "Admin aktivasyonu gerekiyor"
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr "Hesabın onaylanması gerek"
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr "Süper admin"
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr "Admin"
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr "Kullanıcı düzenle"
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr "Aranan kriterde hiç bir kullanıcı bulunamadı."
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr "Kullanıcı ara"
 
@@ -1877,6 +2156,30 @@ msgstr "Hesap şifrenizi değiştirmek için aşağıdaki bağlantıyı tıklay
 msgid "Set new password"
 msgstr "Yeni şifre ayarla"
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -1980,11 +2283,9 @@ msgstr ""
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
 msgstr ""
-"%(user)s, bu ileti Alıyorsunuz, çünkü %(poster)s ,  konu abone olduğunuz "
-"konu %(thread)s yanıt verdi."
 
 #: templates/misago/emails/thread/reply.html:14
 #: templates/misago/emails/thread/reply.txt:10
@@ -1998,11 +2299,9 @@ msgstr "Cevabına git"
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
 msgstr ""
-"%(user)s,%(poster)s, \"%(thread)s\" konu abone olduğunuzdan konu yanıtladığı"
-" için bu ileti alıyorsunuz."
 
 #: templates/misago/errorpages/403.html:40
 msgid "This page is not available."
@@ -2246,7 +2545,7 @@ msgstr "Sonuçlara bak"
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr "Yasaklamak ayrıntıları"
 
@@ -2293,7 +2592,7 @@ msgstr[1] "%(threads)s dizilerini başlattı."
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr "ayrıntılar"
 
@@ -2321,7 +2620,7 @@ msgid "This error is caused by invalid post content manipulation."
 msgstr "Bu hata, geçersiz ileti içeriği manipülasyonundan kaynaklanmaktadır."
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr "Takipçiler"
 
@@ -2349,7 +2648,7 @@ msgid "%(username)s has no followers."
 msgstr "%(username)sTakipçisi yok."
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr "Şöyledir"
 
@@ -2436,7 +2735,7 @@ msgid "%(username)s started no threads."
 msgstr "%(username)sKonu başlatılmamış."
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr "Kullanıcı adı geçmişi"
 
@@ -2463,6 +2762,20 @@ msgstr "Kullanıcı adınız hiçbir zaman değiştirilmedi."
 msgid "%(username)s's username was never changed."
 msgstr "%(username)skullanıcının adı hiç değişmedi."
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr "Arama sitesi"
@@ -2579,10 +2892,6 @@ msgstr " %(hidden_by)starafından gizlendi%(hidden_on)s"
 msgid "By %(event_by)s on %(event_on)s."
 msgstr "%(event_by)s tarafından %(event_on)s"
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr "IP kaydedildi"
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2869,11 +3178,11 @@ msgstr "Ekler"
 msgid "Attachment types"
 msgstr "Ataşman tipleri"
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr "Yeni dosya yüklemek için izniniz yok."
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr "Hiçbir dosya yüklenmedi."
 
@@ -2881,11 +3190,11 @@ msgstr "Hiçbir dosya yüklenmedi."
 msgid "Uploaded image was corrupted or invalid."
 msgstr "Yüklenen resim bozuk veya geçersiz."
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr "Bu tür dosyalar yükleyemezsiniz."
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
@@ -2893,7 +3202,7 @@ msgstr ""
 "%(limit)s daha büyük dosyaları karşıya yükleyemezsiniz "
 "(dosyanızın%(upload)sye sahip olduğu)."
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
@@ -2902,7 +3211,7 @@ msgstr ""
 "Bu türe%(limit)sden büyük dosya yükleyemezsiniz (dosyanızın%(upload)sye "
 "sahip olduğu)."
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr "Bu ileti için düzenleme kayıtları kullanılamıyor."
 
@@ -2918,12 +3227,12 @@ msgstr "Bu konu daki iletiler taşıyamazsınız."
 msgid "You can't like posts in this category."
 msgstr "Bu bölüm iletiler beğenemezsin."
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr "İçerik onayı tersine çevrilebilir."
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr "Güncelleştirilecek bir veya daha fazla iletiler bulunamadı."
 
@@ -2936,7 +3245,7 @@ msgstr "Bu konu dan gelen iletiler bölemezsiniz."
 msgid "You don't have permission to remove \"%(attachment)s\" attachment."
 msgstr "%(attachment)s eki kaldırma izniniz yok."
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -3018,11 +3327,11 @@ msgstr[1] ""
 msgid "One or more users could not be found: %(usernames)s"
 msgstr "Bir veya daha fazla kullanıcı bulunamadı:%(usernames)s"
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr "Bir ileti girmeniz gerekiyor."
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr "Konu başlığını girmeniz gerekiyor."
 
@@ -3102,7 +3411,7 @@ msgstr "Bu kullanıcı zaten konu sahibi."
 msgid "One or more threads to update could not be found."
 msgstr "Güncelleştirilecek bir veya daha fazla konu bulunamadı."
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr "Bu konu daki bir anket zaten var."
 
@@ -3138,7 +3447,7 @@ msgstr "Dosya adı içeriyor"
 msgid "File type"
 msgstr "Dosya tipi"
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr "Belirtmek, bildirmek"
 
@@ -3162,10 +3471,6 @@ msgstr "Dosya uzantıları"
 msgid "Maximum allowed uploaded file size"
 msgstr "Maksimum izin verilen karşıya yüklenen dosya boyutu"
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr "durum"
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr "Yüklemeleri sınırla"
@@ -3811,7 +4116,7 @@ msgstr "Taşkın koruma önlemlerine göre daha sık gönderilmesini sağlar."
 msgid "Can see threads"
 msgstr "Konuları görebilir"
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4718,6 +5023,14 @@ msgstr "Bir veya daha fazla anket seçimi geçersizdi."
 msgid "You have to make a choice."
 msgstr "Bir seçim yapmalısın."
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -4873,23 +5186,13 @@ msgstr "Onaylanmamış içerik listelerini görmek için izniniz yok."
 msgid "Requested attachment could not be found."
 msgstr "İstenen ek bulunamadı."
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr "En yeni gelen"
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr "Eskilerden"
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr "A'dan Z'ye"
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr "Z'den A'ya"
 
@@ -4959,33 +5262,37 @@ msgid ""
 msgstr ""
 "İlk onaylanmamış ileti gidebilmek için içeriğin onaylanması için izin alın."
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr "Kullanıcı hesapları"
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr "Rütbeler"
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr "yasaklar"
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr "%(user)s hesabını %(forum_name)s forumlarında etkinleştir"
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr "%(user)s 'un şifresini %(forum_name)s forumlarında değiştir"
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr "Form bağlantısı geçersiz. Lütfen tekrar deneyin."
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr "Bağlantınızın süresi doldu. Lütfen yeni bir tane talep edin."
 
@@ -5055,15 +5362,15 @@ msgstr "Yeni adrese e-posta değişikliği onay linki gönderildi."
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr "%(forum_name)s forumlarındaki şifre değişikliğini onayla"
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr "Adresinize şifre değişim onay linki gönderildi."
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr "Yeni kullanıcılar kayıtları şu anda kapalıdır."
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr "Lütfen formu tekrar deneyin."
 
@@ -5088,38 +5395,54 @@ msgstr "Kullanıcı adı değiştirilirken hata oluştu. Lütfen tekrar deneyin.
 msgid "You don't have permission to see other users name history."
 msgstr "Diğer kullanıcıların ad geçmişini izlemek için izniniz yok."
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr "Bu işlemi gerçekleştirmek için oturum açmanız gerekir."
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr "Diğer avatarları değiştiremezsiniz."
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr "Diğer kullanıcı seçeneklerini değiştiremezsiniz."
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr "Forum seçenekleriniz değiştirildi."
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr "Diğer kullanıcı adlarını değiştiremezsiniz."
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr "Diğer kullanıcı imzalarını değiştiremezsiniz."
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr "Diğer kullanıcıların şifrelerini değiştiremezsiniz."
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr "Diğer kullanıcıların e-posta adreslerini değiştiremezsiniz."
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr "Detayları düzenle"
@@ -5132,11 +5455,11 @@ msgstr "Kullanıcı adını değiştir"
 msgid "Change email or password"
 msgstr "E-posta veya şifre değiştir"
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -5193,27 +5516,27 @@ msgstr "İzinleri ve grupları düzenle"
 msgid "Edit the user from Misago admin panel"
 msgstr "Misago yönetim masa kullanıcıyı düzenleyin"
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr "Kullanıcı adı"
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr "Özel Başlık"
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr "Mail Adresi"
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr "Kayıtlı tüm üyelerin \"üye\" sıfat olmalıdır."
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr "Yönetici mi"
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to"
@@ -5223,11 +5546,11 @@ msgstr ""
 "Django admin sitesi etkinleştirilmişse, bu kullanıcının admin Django "
 "modüllerine ek izinler atanması gerekecektir."
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr "Süper kullanıcı mı"
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
@@ -5236,11 +5559,11 @@ msgstr ""
 "erişimine ek olarak, superadmins diğer üyelerin yönetim düzeylerini de "
 "değiştirebilir."
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr "Aktif"
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
@@ -5249,11 +5572,11 @@ msgstr ""
 "belirtir. Bu işlevi kapatmak, kullanıcı hesaplarını kaldırmanın yıkılmaz bir"
 " yoludur."
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr "Personel ileti"
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
@@ -5261,15 +5584,15 @@ msgstr ""
 "Forum ekibi üyeleri için kullanıcının hesabının neden devre dışı "
 "bırakıldığını açıklayan isteğe bağlı ileti."
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr "Için şifreyi değiştir"
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr "Avatar kilitle"
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
@@ -5277,12 +5600,12 @@ msgstr ""
 "Bunu evet olarak ayarlamak, kullanıcının avatarını değiştirmesini durduracak"
 " ve avatarını usulle üretilen birine sıfırlayacaktır."
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr "Kullanıcı ileti"
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
@@ -5290,7 +5613,7 @@ msgstr ""
 "Kullanıcı avatar değiştirilme biçiminden neden yasaklandığını açıklayan "
 "isteğe bağlı ileti."
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
@@ -5298,54 +5621,54 @@ msgstr ""
 "Forum ekibi üyeleri için avatarın değişen biçiminin neden kullanıcının "
 "yasaklandığını açıklayan isteğe bağlı ileti."
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr "İmza içeriği"
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr "İmza içeriği"
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr ""
 "Bunu evet olarak ayarlamak, kullanıcının imza üzerinde değişiklik yapmasını "
 "durduracaktır."
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr ""
 "Kullanıcıya kullanıcının imzasının neden kilitlendiğini açıklayan isteğe "
 "bağlı ileti."
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr ""
 "Kullanıcıya kullanıcının imzasının neden kilitlendiğini açıklayan isteğe "
 "bağlı ileti."
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr "Varlığını gizler"
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr "Özel konuları kim kime ekleyebilir?"
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr "Replid iplikleri"
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
 msgstr[0] "İmza %(limit)s karakterden uzun olamaz."
 msgstr[1] "İmza %(limit)s karakterden uzun olamaz."
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
@@ -5353,60 +5676,60 @@ msgstr ""
 "Sıralar, kullanıcıları gruplandırmak ve ayırmak için kullanılır. Ayrıca, "
 "kullanıcı gruplarına izinler eklemek için kullanılırlar."
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr "Roller"
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr ""
 "Bu kullanıcının kişisel rolleri. Tüm kullanıcıların \"üye\" sıfat olmalıdır."
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr "Kullanıcı adı ile başlar"
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr "Ile başlayan e-posta"
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr "kimlik alanları içeriyor"
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr "Yalnızca etkin değil"
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr "Yalnızca devre dışı"
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr "Yalnızca yöneticiler"
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr "Tüm saflar"
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr "Tüm roller"
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr "Rütbe var"
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr "sıfat var"
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
@@ -5414,11 +5737,11 @@ msgstr ""
 "Bu sıralamaya sahip tüm kullanıcıların kısa ve açıklayıcı adı. \"Takım\" "
 "veya \"Oyun Masters\" iyi örneklerdir."
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr "Kullanıcı başlığı"
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example"
 " \"GM\" or \"Dev\"."
@@ -5426,7 +5749,7 @@ msgstr ""
 "Sıralama isminin isteğe bağlı, tekil hali kullanıcı adlarıyla gösterilir. "
 "Örneğin \"GM\" veya \"Dev\"."
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
@@ -5434,19 +5757,19 @@ msgstr ""
 "Bu rütbe ile tanımlanan üyelerin görev veya durumlarını açıklayan isteğe "
 "bağlı açıklama."
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr "Sıra, kullanıcılara ek rol verebilir."
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr "Bu rütbeli sahibine ait içeriğe isteğe bağlı CSS sınıfı eklendi."
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr "Kullanıcı dizelge sıralama sekmesine izin ver"
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
@@ -5455,70 +5778,70 @@ msgstr ""
 "sıralamaya sahip kullanıcılar kolayca başkaları tarafından keşfedilebilir "
 "hale gelecektir."
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr "Bu ad diğer rütbelerle çarpışıyor."
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr "Yasaklamak değerleri"
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
-msgid "Usernames"
-msgstr "Kullanıcı adları"
-
-#: users/forms/admin.py:465 users/forms/admin.py:580
-msgid "E-mails"
-msgstr "E-postalar"
-
-#: users/forms/admin.py:466
-msgid "E-mail domains"
-msgstr "E-posta alan adları"
-
-#: users/forms/admin.py:467
-msgid "IP addresses"
-msgstr "IP adresleri"
-
-#: users/forms/admin.py:468
-msgid "First segment of IP addresses"
-msgstr "IP adreslerinin ilk bölümü"
-
-#: users/forms/admin.py:469
-msgid "First two segments of IP addresses"
-msgstr "IP adreslerinin ilk iki bölümü"
-
-#: users/forms/admin.py:476
+#: users/forms/admin.py:471
 msgid "Optional message displayed to users instead of default one."
 msgstr ""
 "İsteğe bağlı ileti , varsayılan bir kullanıcı yerine kullanıcılara "
 "görüntülenir."
 
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
 msgid "Message can't be longer than 1000 characters."
 msgstr "ileti 1000 karakterden uzun olamaz."
 
-#: users/forms/admin.py:483 users/forms/admin.py:536
+#: users/forms/admin.py:478 users/forms/admin.py:550
 msgid "Team message"
 msgstr "Takım ileti"
 
-#: users/forms/admin.py:486 users/forms/admin.py:539
+#: users/forms/admin.py:481 users/forms/admin.py:553
 msgid "Optional ban message for moderators and administrators."
 msgstr "Yöneticiler ve yöneticiler için isteğe bağlı Yasaklamak ileti."
 
-#: users/forms/admin.py:495
+#: users/forms/admin.py:490
 msgid "Leave this field empty for set bans to never expire."
 msgstr "Set banlarının asla süresi dolmamak için bu alanı boş bırakın."
 
+#: users/forms/admin.py:499 users/forms/admin.py:593
+msgid "Usernames"
+msgstr "Kullanıcı adları"
+
+#: users/forms/admin.py:500 users/forms/admin.py:594
+msgid "E-mails"
+msgstr "E-postalar"
+
 #: users/forms/admin.py:501
+msgid "E-mail domains"
+msgstr "E-posta alan adları"
+
+#: users/forms/admin.py:507
+msgid "IP addresses"
+msgstr "IP adresleri"
+
+#: users/forms/admin.py:508
+msgid "First segment of IP addresses"
+msgstr "IP adreslerinin ilk bölümü"
+
+#: users/forms/admin.py:509
+msgid "First two segments of IP addresses"
+msgstr "IP adreslerinin ilk iki bölümü"
+
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr "Türünü kontrol et"
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr "Bu yasaklamak kayıtlara kısıtla"
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5530,11 +5853,11 @@ msgstr ""
 " süre önce hesaplanan e-posta sağlayıcılarından gelen gibi bazı kayıtları "
 "engellemek istiyorsanız iyi olur."
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr "Yasaklanmış değer"
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
@@ -5544,50 +5867,73 @@ msgstr ""
 "yıldız (*) kabul eder. Örneğin, \"83. *\" değeri için IP Yasaklamak "
 "yapılması, \"83\" ile başlayan tüm IP adreslerini Yasaklamak."
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr "Yasaklanan değer 250 karakterden uzun olamaz."
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr "Yasaklanan ileti 250 karakterden uzun olamaz...."
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr "Bu Yasaklamak hiçbir zaman sona ermemesi için bu alanı boş bırakın."
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr "Yasaklanmış değer çok belirsiz."
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr "Tüm yasaklar"
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr "IP'leri"
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr "Yasaklanmış değer başlıyor"
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr "Sadece kayıt ol"
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr "herhangi"
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr "Aktif"
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr "Süresi doldu"
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case"
+" insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr "Her iki alanı doldurun."
@@ -5644,18 +5990,27 @@ msgid ""
 "request new password."
 msgstr "Yönetici, yeni şifre isteyebilmek için hesabınızı etkinleştirmelidir."
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr "Bu kullanıcı adına izin verilmiyor."
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr "Bu e-posta adresine izin verilmiyor."
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr "Bu IP adresinden yeni kayıtlara izin verilmiyor."
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -5890,7 +6245,7 @@ msgstr ""
 "Her yanıtı yeni satıra yazın. Yanıtlar büyük / küçük harf duyarlı değildir."
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr "Forum ekibi"
 
@@ -5903,48 +6258,60 @@ msgstr "Takım"
 msgid "Members"
 msgstr "Üyeler"
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr "Kullanıcının bir e-posta adresine sahip olması gerekir."
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr "bildirmek"
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr "E-posta ile bildir"
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr "herkes"
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr "Takip ettiğim kullanıcılar"
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr "Kimse"
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr "üzerine katıldı"
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr "personel durumu"
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr ""
 "Kullanıcının admin sitelerine giriş yapıp giriş yapamayacağını belirtir."
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr "aktif"
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6316,11 +6683,7 @@ msgstr "Bu twitter kolunda uygun değil."
 msgid "Join IP"
 msgstr "IP ekle"
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr "Son IP"
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr "%(forum_name)s forumlarına hoşgeldiniz!"
@@ -6362,14 +6725,30 @@ msgstr "Yeni e-mail adresi girmelisiniz."
 msgid "New e-mail is same as current one."
 msgstr "Yeni e-posta, mevcut e-postayla aynı."
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available"
 " for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6470,6 +6849,42 @@ msgstr "Yasaklamak \"%(name)s\" düzenlendi."
 msgid "Ban \"%(name)s\" has been removed."
 msgstr "Yasaklamak \"%(name)s\" kaldırıldı."
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr "İstenen rütbe yok."
@@ -6519,39 +6934,43 @@ msgstr "Sıra \"%(name)s\" zaten varsayılanıdır."
 msgid "Rank \"%(name)s\" has been made default."
 msgstr "Sıra \"%(name)s\" varsayılan olarak ayarlandı."
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr "En büyük posterler"
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr "En küçük posterler"
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr "Kullanıcılar: 0"
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr "Kullanıcıları seç"
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr "Hesapları etkinleştir"
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr "Hesapları sil"
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr "Seçilen kullanıcıları silmek istediğinizden emin misiniz?"
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr "Hepsini sil"
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
@@ -6559,54 +6978,58 @@ msgstr ""
 "Seçilen kullanıcıları silmek istediğinizden emin misiniz? Bu aynı zamanda "
 "hesaplarıyla ilişkili tüm içeriği de silecektir."
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr "Etkin olmayan kullanıcıları seçmelisiniz."
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr "%(forum_name)s forumlarındaki hesabınız etkinleştirildi"
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr "Seçilen kullanıcı hesapları etkinleştirildi."
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr "%(user)ssüper yönetici ve yasaklanamaz."
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr "Seçilen kullanıcılar yasaklandı."
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr "Kendini silemezsin."
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr "%(user)s admin ve silinemiyor."
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr "Seçilen kullanıcılar silindi."
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr "Yeni kullanıcı \"%(user)s\" kaydedildi."
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr "Kullanıcı \"%(user)s\" düzenlendi."
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr "Bu işlem direk olarak gerçekleştirilemez."
 

BIN
misago/locale/tr/LC_MESSAGES/djangojs.mo


+ 285 - 207
misago/locale/tr/LC_MESSAGES/djangojs.po

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-20 21:20+0200\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Bilal Akgül <billakgl@gmail.com>, 2018\n"
 "Language-Team: Turkish (Turkey) (https://www.transifex.com/misago/teams/65369/tr_TR/)\n"
@@ -39,8 +39,30 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
-msgstr "Kayıt olarak sitenin şart ve koşullarını kabul etmiş olursunuz."
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
+msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
@@ -48,11 +70,12 @@ msgstr "Kayıt olarak sitenin şart ve koşullarını kabul etmiş olursunuz."
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr "Kapat"
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr "Katılımcı ekle"
 
@@ -70,9 +93,10 @@ msgstr "Eklenecek kullanıcı"
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr "İptal et"
 
@@ -198,8 +222,8 @@ msgid "Generate my individual avatar"
 msgstr "Avatarımı oluştur"
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr "Tamam"
 
@@ -267,40 +291,40 @@ msgstr "Kod ekle"
 msgid "Emphase selection"
 msgstr "Vurgu seçimi"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr "Yatay cetvel ekle"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr "Resmin linkini gir"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr "Resim etiketi girin (isteğe bağlı)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr "Resim Ekle"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr "Bağlantı adresi girin"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr "Bağlantı etiketini girin (isteğe bağlı)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr "Bağlantı Ekle"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr ""
 "@ ile kullanıcı adlarını veya alıntı yapılacak yazarın adını yazabilirsiniz"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr "Alıntı yap"
 
@@ -333,7 +357,7 @@ msgstr "Silme işlemini geri al"
 msgid "Error uploading %(filename)s"
 msgstr "Yüklenirken %(filename)s hata"
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr "Reddet"
 
@@ -350,7 +374,7 @@ msgid "Protected"
 msgstr "Korumalı"
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr "Koru"
 
@@ -385,7 +409,7 @@ msgid ""
 " deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr "Anket"
 
@@ -400,6 +424,7 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr "Konular birleştir"
 
@@ -429,8 +454,7 @@ msgstr ""
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr "%(title)s, %(joined_on)s'da katıldı."
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr "Kullanıcı adını değiştir"
 
@@ -467,7 +491,7 @@ msgstr[1] ""
 msgid "Your new username is same as current one."
 msgstr "Kullanici ismin şuankiyle ayni."
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr "Yeni kullanıcı adı"
 
@@ -476,15 +500,15 @@ msgid "Your username has been changed successfully."
 msgstr "Kullanıcı adin başarıyla değiştirildi."
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr "Seçeneklerini değiştir"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr "Hesabı sil"
 
@@ -520,7 +544,54 @@ msgstr ""
 msgid "Delete my account"
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr "Detayların güncellendi."
 
@@ -605,7 +676,7 @@ msgstr "Başlattığım konular"
 msgid "Threads I reply to"
 msgstr "Cevapladığım konular"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr "Mail adresini ve şifreni değiştir"
 
@@ -655,7 +726,7 @@ msgstr "Yeni şifre"
 msgid "Repeat password"
 msgstr "Şifreni tekrarla"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr "Unutulan şifreni değiştir"
 
@@ -845,21 +916,21 @@ msgid_plural "%(votes)s votes."
 msgstr[0] "%(votes)s oy."
 msgstr[1] "%(votes)s oy."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr "Oylar halka açıktır."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] "%(votes)s oy, toplamı içinde %%(proc)s "
 msgstr[1] "%(votes)s oy, toplamı içinde %%(proc)s "
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr "Senin seçimin."
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] "%(votes)s adet kullanıcı bu seçim için oy kullandı."
@@ -877,9 +948,8 @@ msgstr "Oy ver"
 msgid "See votes"
 msgstr "Oyları gör"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr "Düzenle"
 
@@ -889,8 +959,7 @@ msgid ""
 msgstr "Bu anketi silmek istediğinizden emin misiniz? Bu eylem geri alınamaz."
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr "Silmek"
 
@@ -954,27 +1023,27 @@ msgstr "İleti önceki duruma döndü."
 msgid "See previous change"
 msgstr "Önceki değişikliği gör"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr "%(edited_by)s tarafından %(edited_on)s tarihinde düzenlendi."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr "Bu yazı içeriği görüntülenemiyor."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr "Bu hata, geçersiz yayın içeriği manipülasyonundan kaynaklanmaktadır."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr "%(posted_on)s tarihinde yayımlandı."
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr "Kaldırılan kullanıcı"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr "İletiyi gör"
 
@@ -996,7 +1065,7 @@ msgstr "Bu iletiyi hiç kimse beğenmedi."
 msgid "Are you sure you want to discard changes?"
 msgstr "Değişiklikleri silmek istediğinden emin misin?"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr "Bir mesaj girmeniz gerekiyor."
 
@@ -1028,11 +1097,12 @@ msgstr "Özel konu, silmek istediğinden emin misin?"
 msgid "You have to enter at least one recipient."
 msgstr "En az bir alıcı girmelisiniz."
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr "Konu başlığı girmelisiniz."
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr "Senin konu yayımlanmıştır."
 
@@ -1040,12 +1110,13 @@ msgstr "Senin konu yayımlanmıştır."
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr "Virgülle ayrılmış kullanıcı isimleri liste si, örneğin: Danny, Lisa"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr "Konu başlığı"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr "Konu Aç"
 
@@ -1053,35 +1124,35 @@ msgstr "Konu Aç"
 msgid "Are you sure you want to discard thread?"
 msgstr "Konuyu silmek istediğinden emin misin?"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr "Kapalı"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr "Açık"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr "Gizlenmiş"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr "Gizlenmemiş"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr "sabitlemeyi kaldır."
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr "Yerel olarak sabitlenmiş"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1144,12 +1215,12 @@ msgstr[1] ""
 "karakterdir)"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr "Gizle"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr "Gizleme"
 
@@ -1171,10 +1242,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr "%(event_by)s tarafından %(event_on)s tarihinde."
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr "IP kaydedildi"
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr "%(old_title)s  konu başlığı değiştirildi."
 
@@ -1264,23 +1331,23 @@ msgstr ""
 msgid "Post has been deleted."
 msgstr "İleti silindi."
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr "Bu ileti için kalıcı bağlantı:"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr "Kalıcı bağlantı"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] "Bu ileti %(edits)s tarihinde düzenlendi."
@@ -1290,17 +1357,15 @@ msgstr[1] "Bu ileti %(edits)s tarihinde düzenlendi."
 msgid "Changes history"
 msgstr "Geçmişi değiştirir"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr "Onayla"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr "Taşı"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr "Ayır"
 
@@ -1313,6 +1378,7 @@ msgid "Move post"
 msgstr "İleti yi taşı"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr "Diğer konu linkini girmelisin"
 
@@ -1362,7 +1428,7 @@ msgid "Close thread"
 msgstr "Kapalı konu"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr "Bölüm"
 
@@ -1414,32 +1480,32 @@ msgid_plural "%(users)s and %(likes)s other users like this."
 msgstr[0] "%(users)s ve %(likes)s diğer kullanıcılar"
 msgstr[1] "%(users)s ve %(likes)s diğer kullanıcılar"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr "Beğenilen"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr "Beğeni"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr "Cevapla"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr "Yeni ileti"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr "Yeni"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr "Bu korunan bir ileti ve düzenlenmez."
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr "korunan"
 
@@ -1487,69 +1553,68 @@ msgstr "Başkalarıyla herhangi bir ayrıntı paylaşmıyorsunuz."
 msgid "%(username)s is not sharing any details with others."
 msgstr "%(username)s başkalarıyla herhangi bir ayrıntı paylaşmıyor."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr "Ayrıntılar"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr "%(username)skullanıcısının detayları güncellendi."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr "Başlatılmış sana ait konular yok."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr "%(username)s kullanıcısına ait başlatılmış konular yok"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] "Sana ait %(threads)s adet başlatılmış konular var."
 msgstr[1] "Sana ait %(threads)s adet başlatılmış konular var."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] "%(username)s adlı kullanıcı %(threads)skonular ını başlattı."
 msgstr[1] "%(username)s adlı kullanıcı %(threads)skonular ını başlattı."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr "Yükleniyor..."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr "Konular"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr "Gönderilmiş bir mesaj yok."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr "%(username)s kullanıcısının gönderdiği bir mesaj yok."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] "Gönderilmiş %(posts)s mesajın var."
 msgstr[1] "Gönderilmiş %(posts)s mesajın var."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] "%(username)s kullanıcısının %(posts)s mesajı var."
 msgstr[1] "%(username)s kullanıcısının %(posts)s mesajı var."
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr "İletiler"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr "Daha fazlasını gör (%(more)s)"
 
@@ -1640,6 +1705,7 @@ msgid "Joined %(joined_on)s"
 msgstr "%(joined_on)s katıldı."
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr "Moderasyon"
 
@@ -1699,44 +1765,44 @@ msgstr ""
 msgid "Avatar controls"
 msgstr "Avatar kontrolleri"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr "Kullanıcı adınız değiştirildi."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr ""
 "%(username)skullanıcısının hesabı, konular, iletiler ve diğer içerikleri "
 "silindi."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr ""
 "%(username)skullanıcısının hesabı silindi ve diğer içerikleri gizlendi."
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr "%(username)s sil"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr "Lütfen bekleyin... (%(countdown)s)"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr "Kullanıcı içeriği"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr "Kullanıcının hesabıyla birlikte sil"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr "Kullanıcının hesabını sildikten sonra gizle"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr "Kullanıcı liste sine geri dön"
 
@@ -1802,32 +1868,32 @@ msgstr "Kayıt şu anda bir hata nedeniyle kullanılamıyor."
 msgid "Register"
 msgstr "Kayıt Ol"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr "Kullanıcı Adı"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr "E-posta"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr "Şifre"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr "Hesabı Kaydet"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
@@ -1835,7 +1901,7 @@ msgstr ""
 "%(username)s hesabınız oluşturuldu, ancak oturum açabilmeniz için önce "
 "etkinleştirmeniz gerekir."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
@@ -1843,7 +1909,7 @@ msgstr ""
 "%(username)shesabınız oluşturuldu fakat pano yöneticisi oturum açabilmeniz "
 "için onu aktif hale getirmek zorundadır. "
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
@@ -1851,32 +1917,31 @@ msgstr ""
 "Hesabınızı aktifleştirmek için gerekli linki %(email)s adresinize e-posta "
 "yolladık."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr "Bu gerçekleştiğinde %(email)sadresinize e-posta göndereceğiz."
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr "Kayıt tamamlandı."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr "Geçerli bir e-posta adresi girin."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr "E-posta Adresiniz"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr "Link gönder"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr "Aktivasyon linki %(email)s adresinize gönderildi."
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr "Başka bir link iste"
 
@@ -1941,41 +2006,41 @@ msgstr "Arama sorgusuyla eşleşen hiçbir kullanıcı bulunamadı."
 msgid "Enter at least two characters to search users."
 msgstr "Kullanıcı aramak için en az iki karakter girin."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr "Her iki alanı doldurun."
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr "Hesabı etkinleştir"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr "Kullanıcı adı veya e-posta"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr "Şifreyi mi unuttun?"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to "
+"%(username)s, your account has been created and you have been signed in to "
 "it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -1984,6 +2049,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -2004,7 +2073,7 @@ msgid "Edit title"
 msgstr "başlığı düzenlemek"
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr "Onaylanmamış"
 
@@ -2012,7 +2081,7 @@ msgstr "Onaylanmamış"
 msgid "Unapproved posts"
 msgstr "Onaylanmamış iletiler"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] "%(replies)s adet cevap"
@@ -2038,19 +2107,19 @@ msgstr ""
 "Seçilen iletileri silmek istediğinizden emin misiniz? Bu eylem geri "
 "alınamaz!"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr "kaynaşmak"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr "Korumalı değil"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr "%(posted_on)s'de%(username)s"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr "Bir veya daha fazla iletiler değiştirilemedi:"
 
@@ -2110,19 +2179,19 @@ msgstr "Yerel olarak sabitlenmiş"
 msgid "Unpin"
 msgstr "Sabitlemeyi kaldır."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr "Birleştirilmiş konu"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr "Konu, başka bir konu ile birleştirildi."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr "Birleştirmek istediğiniz konu linkini ver"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
@@ -2130,19 +2199,19 @@ msgstr ""
 "Birleştirme yapılırken geçerli konu silinir ve burada belirtilen konu "
 "içeriğine taşınır."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr "Konu taşı"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr "Bu konu şu anda taşınamaz."
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr "Konu taşındı."
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr "Yeni bölüm"
 
@@ -2180,15 +2249,15 @@ msgstr "İzin Ver"
 msgid "Disabled"
 msgstr "çevrimdışı"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr "Abonelikten Çık"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr "abone"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr "E-posta ile abone ol"
 
@@ -2220,11 +2289,11 @@ msgstr "Son ileti"
 msgid "Options"
 msgstr "Seçenkler"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr "Anket ekle"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2249,7 +2318,7 @@ msgstr ""
 msgid "Change subscription"
 msgstr "Aboneliği değiştir"
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr "Konu başlat"
 
@@ -2329,7 +2398,7 @@ msgstr "Yerele sabitlenmiş konular"
 msgid "Unpin threads"
 msgstr "Sabitlenmesi kaldırılmış konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr "Konular taşı"
 
@@ -2365,7 +2434,7 @@ msgstr "Konular moderasyonu"
 msgid "One or more threads could not be deleted:"
 msgstr "Bir veya daha fazla konular silinemedi:"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2380,11 +2449,11 @@ msgstr ""
 "Konular ı birleştirmek için bölüm içindeki konular a başlatma izni almanız "
 "gerekir."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr "Seçilen konular taşındı."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
@@ -2392,51 +2461,51 @@ msgstr ""
 "Konular ı taşımak için bölüm içindeki konular a başlatma izni almanız "
 "gerekir."
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr "Tümünü seç"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr "Birini seç"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr "Tümü"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr "Tüm konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr "Benim"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr "Benim konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr "Yeni Konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr "Okunmamış"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr "Okunmamış konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr "Abone olundu"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr "Abone olunan konular"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr "Onaylanmamış içerik"
 
@@ -2453,7 +2522,8 @@ msgstr ""
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr "Özel Konular"
 
@@ -2461,67 +2531,67 @@ msgstr "Özel Konular"
 msgid "Are you sure you want to sign out?"
 msgstr "Oturumu kapatmak istediğinizden emin misiniz?"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr "Profil Gör"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr "Seçenekleri değiştir"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr "Avatarı değiştir"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr "Çıkış yap"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr "%(username)s kullanıcısı %(ban_expires)s e kadar yasaklanmıştır."
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr "%(username)s yasaklıdır."
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr "%(username)s gizli varlıktır."
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr "%(username)s çevrimiçidir (gizlenmiş)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr "%(username)s son görülmesi %(last_click)s (gizlenmiş)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr "%(username)s çevrimiçidir"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr "%(username)s son görülmesi %(last_click)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr "Yasaklanmış"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr "Çevrimiçi (gizlenmiş)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr "Çevrimdışı (gizlenmiş)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr "Çevrimiçi"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr "Çevrimdışı"
 
@@ -2535,19 +2605,19 @@ msgstr[1] "%(followers)s takipçiler"
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr "Hiçbir kullanıcı son %(days)s gün içinde yeni mesaj göndermedi."
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr "Rütbe"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr "Rütbeli iletiler"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr "Toplam iletiler"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] "son %(days)s günlerden en aktif posterleri %(posters)s"
@@ -2579,7 +2649,7 @@ msgstr "evet"
 msgid "no"
 msgstr "hayır"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
@@ -2587,19 +2657,19 @@ msgstr ""
 "Özel konular, sadece onları başlatan ve katılmak için davet edilmiş "
 "olanların görebileceği konular demektir."
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr "Özel konular, katılamazsın."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr "Uygulamayla bağlantı kesildi."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr "Aktivasyon linki geçersizdir."
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr "Bilinmeyen bir hata oluştu."
 
@@ -2636,15 +2706,23 @@ msgstr "Zaten bir anket üzerinde çalışıyorsun. Çöpe atmak ister misin?"
 msgid "You don't have permission to perform this action."
 msgstr "Bu eylemi gerçekleştirmek için iznin yok."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr "Yasaklandin."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr "Bu alan gerekli."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2658,7 +2736,7 @@ msgstr[1] ""
 "Bu değerin en az %(limit_value)s karakterine sahip olduğundan emin olun "
 "(%(show_value)s olmalıdır)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2672,7 +2750,7 @@ msgstr[1] ""
 "Bu değerin en çok %(limit_value)s karakterine sahip olduğundan emin olun "
 "(%(show_value)s olmalıdır)."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] ""
@@ -2680,18 +2758,18 @@ msgstr[0] ""
 msgstr[1] ""
 "Kullanıcı adı en az %(limit_value)s karakter uzunluğunda olmalıdır."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] "Kullanıcı adı %(limit_value)s karakterden uzun olamaz."
 msgstr[1] "Kullanıcı adı %(limit_value)s karakterden uzun olamaz."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr ""
 "Kullanıcı adı sadece latin alfabesindeki harfleri ve rakamları içerebilir."
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural ""
 "Valid password must be at least %(limit_value)s characters long."

BIN
misago/locale/zh_Hans/LC_MESSAGES/django.mo


+ 738 - 311
misago/locale/zh_Hans/LC_MESSAGES/django.po

@@ -8,9 +8,9 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-31 23:36+0000\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: Neo Chang <me@neoto.xin>, 2017\n"
+"Last-Translator: cxgreat2014 <fwy1998@gmail.com>, 2018\n"
 "Language-Team: Chinese (China) (https://www.transifex.com/misago/teams/65369/zh_CN/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -25,7 +25,7 @@ msgstr ""
 msgid "Permissions"
 msgstr "权限"
 
-#: acl/admin.py:33 users/forms/admin.py:414
+#: acl/admin.py:33 users/forms/admin.py:416
 msgid "User roles"
 msgstr "用户身份"
 
@@ -142,17 +142,17 @@ msgstr "您必须选择一个或者多个项目"
 msgid "Action is not allowed."
 msgstr "操作不被许可"
 
-#: admin/views/index.py:70
+#: admin/views/index.py:83
 #, python-format
 msgid "Outdated: %(current)s! (latest: %(latest)s)"
 msgstr "已过期:%(current)s!(最新:%(latest)s)"
 
-#: admin/views/index.py:78
+#: admin/views/index.py:91
 #, python-format
 msgid "Up to date! (%(current)s)"
 msgstr "更新到 (%(current)s)"
 
-#: admin/views/index.py:87
+#: admin/views/index.py:100
 msgid "Failed to connect to GitHub API. Try again later."
 msgstr "访问GitHub API失败,请稍候尝试"
 
@@ -171,11 +171,11 @@ msgstr "版块层次"
 msgid "Category roles"
 msgstr "版块身份"
 
-#: categories/forms.py:47 users/forms/admin.py:388
+#: categories/forms.py:47 users/forms/admin.py:390
 msgid "Name"
 msgstr "名称"
 
-#: categories/forms.py:49 users/forms/admin.py:404
+#: categories/forms.py:49 users/forms/admin.py:406
 msgid "Description"
 msgstr "描述"
 
@@ -183,7 +183,7 @@ msgstr "描述"
 msgid "Optional description explaining category intented purpose."
 msgstr "解释版块用途(可选)。"
 
-#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:421
+#: categories/forms.py:56 categories/forms.py:68 users/forms/admin.py:423
 msgid "CSS class"
 msgstr "CSS类"
 
@@ -201,7 +201,7 @@ msgstr "已关闭版块"
 msgid "Only members with valid permissions can post in closed categories."
 msgstr "只有具备特定权限的成员可以在已经关闭的版块中发帖。"
 
-#: categories/forms.py:75 templates/misago/admin/index.html:83
+#: categories/forms.py:75 templates/misago/admin/index.html:110
 #: templates/misago/admin/users/delete.html:32 templates/misago/navbar.html:18
 #: templates/misago/navbar.html:34 templates/misago/profile/threads.html:5
 #: templates/misago/profile/threads.html:8
@@ -214,7 +214,7 @@ msgstr "只有具备特定权限的成员可以在已经关闭的版块中发帖
 #: threads/migrations/0002_threads_settings.py:16
 #: threads/migrations/0004_update_settings.py:16
 #: threads/permissions/threads.py:72 threads/permissions/threads.py:103
-#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:89
+#: threads/search.py:20 threads/viewmodels/thread.py:118 users/apps.py:97
 msgid "Threads"
 msgstr "主题帖"
 
@@ -511,7 +511,7 @@ msgstr "请求验证不可用"
 
 #: core/forms.py:43 templates/misago/admin/users/edit.html:61
 #: templates/misago/admin/users/edit.html:78
-#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:590
+#: templates/misago/admin/users/edit.html:184 users/forms/admin.py:604
 msgid "Yes"
 msgstr "是"
 
@@ -525,7 +525,7 @@ msgstr "是"
 #: threads/permissions/threads.py:164 threads/permissions/threads.py:178
 #: threads/permissions/threads.py:189 threads/permissions/threads.py:205
 #: threads/permissions/threads.py:216 threads/permissions/threads.py:247
-#: users/forms/admin.py:591 users/models/user.py:145
+#: users/forms/admin.py:605 users/models/user.py:147
 msgid "No"
 msgstr "否"
 
@@ -623,7 +623,72 @@ msgstr "必须包括字母或者数字"
 msgid "Value is too long."
 msgstr "过长"
 
+#: legal/admin.py:25 templates/misago/admin/users/edit.html:216
+msgid "Agreements"
+msgstr ""
+
+#: legal/api.py:18
+msgid "You have already accepted this agreement."
+msgstr ""
+
+#: legal/api.py:27
+msgid "You need to submit a valid choice."
+msgstr ""
+
+#: legal/forms.py:10 legal/forms.py:60
+#: templates/misago/admin/agreements/list.html:18
+#: templates/misago/admin/attachmenttypes/list.html:16
+#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:589
+msgid "Type"
+msgstr "类型"
+
+#: legal/forms.py:12 templates/misago/admin/agreements/list.html:16
+#: templates/misago/admin/ranks/list.html:17
+msgid "Title"
+msgstr "标题"
+
+#: legal/forms.py:13
+msgid "Optional, leave empty for agreement to be named after its type."
+msgstr ""
+
+#: legal/forms.py:17
+msgid "Set as active for its type"
+msgstr ""
+
+#: legal/forms.py:19
+msgid ""
+"If other agreement is already active for this type, it will be unset and "
+"replaced with this one. Misago will ask users who didn't accept this "
+"agreement to do so before allowing them to continue using the site's "
+"features."
+msgstr ""
+
+#: legal/forms.py:27
+msgid "Link"
+msgstr ""
+
+#: legal/forms.py:28
+msgid "If your agreement is located on other page, enter here a link to it."
+msgstr ""
+
+#: legal/forms.py:32
+msgid "Text"
+msgstr ""
+
+#: legal/forms.py:33
+msgid "You can use Markdown syntax for rich text elements."
+msgstr ""
+
+#: legal/forms.py:46
+msgid "Please fill in agreement link or text."
+msgstr ""
+
+#: legal/forms.py:65
+msgid "Content"
+msgstr ""
+
 #: legal/migrations/0001_initial.py:16
+#: legal/migrations/0003_create_agreements_from_settings.py:57
 msgid "Legal information"
 msgstr "法律信息"
 
@@ -666,7 +731,7 @@ msgstr "您的论坛可以有自定义的服务条款页面。如果要创建它
 msgid "Policy title"
 msgstr "政策标题"
 
-#: legal/migrations/0001_initial.py:64 legal/views.py:46
+#: legal/migrations/0001_initial.py:64 legal/models.py:43
 #: templates/misago/footer.html:27
 msgid "Privacy policy"
 msgstr "隐私政策"
@@ -690,21 +755,85 @@ msgid ""
 msgstr "您的论坛可以有隐私政策页面。可以在此处粘贴他的内容。可使用框架标记语言"
 
 #: legal/migrations/0001_initial.py:105
+#: legal/migrations/0003_create_agreements_from_settings.py:62
 msgid "Footnote"
 msgstr "补充说明"
 
 #: legal/migrations/0001_initial.py:106
+#: legal/migrations/0003_create_agreements_from_settings.py:63
 msgid "Short message displayed in forum footer."
 msgstr "展示在论坛底部的短消息"
 
 #: legal/migrations/0001_initial.py:107
+#: legal/migrations/0003_create_agreements_from_settings.py:64
 msgid "Forum footer"
 msgstr "论坛底部"
 
-#: legal/views.py:65 templates/misago/footer.html:22
+#: legal/migrations/0003_create_agreements_from_settings.py:58
+msgid ""
+"Those settings allow you to set additional legal information for your forum."
+msgstr ""
+
+#: legal/models.py:42 templates/misago/footer.html:22
 msgid "Terms of service"
 msgstr "服务条款"
 
+#: legal/views/admin.py:17
+msgid "Requested agreement does not exist."
+msgstr ""
+
+#: legal/views/admin.py:29 threads/views/admin/attachments.py:24
+#: users/views/admin/bans.py:24 users/views/admin/datadownloads.py:20
+#: users/views/admin/users.py:56
+msgid "From newest"
+msgstr "从最新注册的"
+
+#: legal/views/admin.py:30 threads/views/admin/attachments.py:25
+#: users/views/admin/bans.py:25 users/views/admin/datadownloads.py:21
+#: users/views/admin/users.py:57
+msgid "From oldest"
+msgstr "从最早注册的"
+
+#: legal/views/admin.py:33
+msgid "With agreements: 0"
+msgstr ""
+
+#: legal/views/admin.py:34
+msgid "Select agreements"
+msgstr ""
+
+#: legal/views/admin.py:38
+msgid "Delete agreements"
+msgstr ""
+
+#: legal/views/admin.py:39
+msgid "Are you sure you want to delete those agreements?"
+msgstr ""
+
+#: legal/views/admin.py:49
+msgid "Selected agreements have been deleted."
+msgstr ""
+
+#: legal/views/admin.py:53
+#, python-format
+msgid "New agreement \"%(title)s\" has been saved."
+msgstr ""
+
+#: legal/views/admin.py:63
+#, python-format
+msgid "Agreement \"%(title)s\" has been edited."
+msgstr ""
+
+#: legal/views/admin.py:77
+#, python-format
+msgid "Agreement \"%(title)s\" has been deleted."
+msgstr ""
+
+#: legal/views/admin.py:85
+#, python-format
+msgid "Agreement \"%(title)s\" has been set as active for type \"%(type)s\"."
+msgstr ""
+
 #: markup/finalise.py:22
 #, python-format
 msgid "%(title)s has written:"
@@ -714,15 +843,15 @@ msgstr "%(title)s 说:"
 msgid "Quoted message:"
 msgstr "引用消息"
 
-#: project_template/project_name/settings.py:388
+#: project_template/project_name/settings.py:424
 msgid "Personal"
 msgstr "个人"
 
-#: project_template/project_name/settings.py:397
+#: project_template/project_name/settings.py:433
 msgid "Contact"
 msgstr "联系"
 
-#: project_template/project_name/settings.py:405 users/models/ban.py:76
+#: project_template/project_name/settings.py:441 users/models/ban.py:76
 msgid "IP address"
 msgstr "IP地址"
 
@@ -821,6 +950,98 @@ msgstr "激活失败"
 msgid "Your account can't be activated at this time."
 msgstr "您的账号本次不能被激活。"
 
+#: templates/misago/admin/agreements/form.html:9
+#: templates/misago/admin/agreements/form.html:18
+#: templates/misago/admin/agreements/form.html:28
+#: templates/misago/admin/agreements/list.html:9
+msgid "New agreement"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:43
+#: templates/misago/admin/categoryroles/form.html:43
+#: templates/misago/admin/roles/form.html:43
+msgid "Basic settings"
+msgstr "基本设置"
+
+#: templates/misago/admin/agreements/form.html:51
+msgid "Agreement contents"
+msgstr ""
+
+#: templates/misago/admin/agreements/form.html:54
+msgid "Fill in one of the fields."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:19
+msgid "Created"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:20
+msgid "Modified"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:33
+#, python-format
+msgid "Active %(type)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:52
+#, python-format
+msgid "%(created_on)s by %(created_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:68
+#, python-format
+msgid "%(last_modified_on)s by %(last_modified_by)s"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:72
+msgid "never"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:78
+msgid "Set as active"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:88
+#: templates/misago/admin/attachmenttypes/list.html:54
+#: templates/misago/admin/bans/list.html:55
+#: templates/misago/admin/categories/list.html:66
+#: templates/misago/admin/categoryroles/list.html:27
+#: templates/misago/admin/ranks/list.html:99
+#: templates/misago/admin/roles/list.html:43
+#: templates/misago/admin/warnings/list.html:109
+#: templates/misago/poll/results.html:69
+#: templates/misago/profile/details.html:24
+#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
+msgid "Edit"
+msgstr "编辑"
+
+#: templates/misago/admin/agreements/list.html:94
+#: templates/misago/admin/bans/list.html:61
+msgid "Remove"
+msgstr "移除"
+
+#: templates/misago/admin/agreements/list.html:106
+msgid "No agreements matching search criteria have been found"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:108
+msgid "No agreements are currently set."
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:119
+msgid "Are you sure you want to set this agreement as active for its type?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:124
+msgid "Are you sure you want to delete this agreement?"
+msgstr ""
+
+#: templates/misago/admin/agreements/list.html:133
+#: templates/misago/admin/bans/list.html:95
+msgid "Search bans"
+msgstr "搜索封禁"
+
 #: templates/misago/admin/attachments/list.html:7
 msgid "Attachment"
 msgstr "附件"
@@ -831,11 +1052,8 @@ msgstr "主题帖"
 
 #: templates/misago/admin/attachments/list.html:42
 #, python-format
-msgid ""
-"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s from "
-"%(uploader_ip)s."
+msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
 msgstr ""
-"%(filetype)s, %(size)s, 上传者%(uploader)s %(uploaded_on)s 源IP:%(uploader_ip)s."
 
 #: templates/misago/admin/attachments/list.html:53
 #: templates/misago/admin/attachmenttypes/list.html:47
@@ -885,11 +1103,6 @@ msgstr "基本选项"
 msgid "Availability"
 msgstr "可用性(权限)"
 
-#: templates/misago/admin/attachmenttypes/list.html:16
-#: templates/misago/admin/bans/list.html:17 users/forms/admin.py:575
-msgid "Type"
-msgstr "类型"
-
 #: templates/misago/admin/attachmenttypes/list.html:17
 msgid "Extensions"
 msgstr "扩展"
@@ -902,19 +1115,6 @@ msgstr "MIME类型"
 msgid "Files"
 msgstr "文件"
 
-#: templates/misago/admin/attachmenttypes/list.html:54
-#: templates/misago/admin/bans/list.html:55
-#: templates/misago/admin/categories/list.html:66
-#: templates/misago/admin/categoryroles/list.html:27
-#: templates/misago/admin/ranks/list.html:99
-#: templates/misago/admin/roles/list.html:43
-#: templates/misago/admin/warnings/list.html:109
-#: templates/misago/poll/results.html:69
-#: templates/misago/profile/details.html:24
-#: templates/misago/thread/posts/post/footer.html:31 users/djangoadmin.py:54
-msgid "Edit"
-msgstr "编辑"
-
 #: templates/misago/admin/attachmenttypes/list.html:71
 msgid "No attachment types are currently defined."
 msgstr "当前没有定义任何附件类型。"
@@ -931,12 +1131,12 @@ msgid "New ban"
 msgstr "新的封禁"
 
 #: templates/misago/admin/bans/form.html:43
-#: templates/misago/admin/users/ban.html:53
+#: templates/misago/admin/users/ban.html:57
 msgid "Ban settings"
 msgstr "封禁设置"
 
 #: templates/misago/admin/bans/form.html:52
-#: templates/misago/admin/users/ban.html:60
+#: templates/misago/admin/users/ban.html:64
 msgid "Messages"
 msgstr "消息"
 
@@ -944,8 +1144,8 @@ msgstr "消息"
 msgid "Ban"
 msgstr "封禁"
 
-#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:493
-#: users/forms/admin.py:546
+#: templates/misago/admin/bans/list.html:18 users/forms/admin.py:488
+#: users/forms/admin.py:560
 msgid "Expires on"
 msgstr "过期于"
 
@@ -958,10 +1158,6 @@ msgstr "%(check_type)s, 只允许注册"
 msgid "Never"
 msgstr "从不"
 
-#: templates/misago/admin/bans/list.html:61
-msgid "Remove"
-msgstr "移除"
-
 #: templates/misago/admin/bans/list.html:73
 msgid "No bans matching search criteria have been found"
 msgstr "没有符合搜索要求的封禁"
@@ -974,10 +1170,6 @@ msgstr "目前没有设置封禁"
 msgid "Are you sure you want to remove this ban?"
 msgstr "确认移除此封禁?"
 
-#: templates/misago/admin/bans/list.html:95
-msgid "Search bans"
-msgstr "搜索封禁"
-
 #: templates/misago/admin/base_thin.html:8
 msgid "Misago Administration"
 msgstr "论坛框架管理"
@@ -1111,11 +1303,6 @@ msgstr "确认放弃所有更改?"
 msgid "New role"
 msgstr "新身份"
 
-#: templates/misago/admin/categoryroles/form.html:43
-#: templates/misago/admin/roles/form.html:43
-msgid "Basic settings"
-msgstr "基本设置"
-
 #: templates/misago/admin/categoryroles/list.html:16
 msgid "Category role"
 msgstr "版块身份"
@@ -1153,6 +1340,61 @@ msgstr ""
 msgid "Change settings"
 msgstr "更改设置"
 
+#: templates/misago/admin/datadownloads/form.html:6
+#: templates/misago/admin/datadownloads/form.html:11
+#: templates/misago/admin/datadownloads/form.html:17
+msgid "Request new data downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:9
+msgid "Request new downloads"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:17
+#: templates/misago/admin/users/list.html:17 users/forms/admin.py:696
+msgid "User"
+msgstr "用户"
+
+#: templates/misago/admin/datadownloads/list.html:18 threads/forms.py:56
+#: users/forms/admin.py:691
+msgid "Status"
+msgstr "状态"
+
+#: templates/misago/admin/datadownloads/list.html:19
+msgid "Requested on"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:21 users/forms/admin.py:700
+msgid "Requested by"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:32
+#: templates/misago/admin/datadownloads/list.html:49
+#: templates/misago/admin/datadownloads/list.html:52
+#: templates/misago/admin/users/ban.html:35
+#: templates/misago/admin/users/edit.html:113
+#: templates/misago/admin/users/list.html:35
+#: templates/misago/userslists/active_posters.html:66
+msgid "Avatar"
+msgstr "头像"
+
+#: templates/misago/admin/datadownloads/list.html:73
+#: templates/misago/emails/data_download.html:11 users/apps.py:50
+msgid "Download data"
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:84
+msgid "No data downloads matching search criteria have been found."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:86
+msgid "No data downloads exist at the moment."
+msgstr ""
+
+#: templates/misago/admin/datadownloads/list.html:93
+msgid "Search data downloads"
+msgstr ""
+
 #: templates/misago/admin/errorpages/403.html:5
 #: templates/misago/errorpages/403.html:5
 msgid "Page not available"
@@ -1308,44 +1550,73 @@ msgstr "最后"
 msgid "Administration Home"
 msgstr "管理员主页"
 
-#: templates/misago/admin/index.html:33
+#: templates/misago/admin/index.html:22
+msgid "System checks"
+msgstr ""
+
+#: templates/misago/admin/index.html:26
+msgid "MISAGO_ADDRESS setting appears to be correct."
+msgstr ""
+
+#: templates/misago/admin/index.html:30
+msgid "The settings.py value for MISAGO_ADDRESS appears to be incorrect."
+msgstr ""
+
+#: templates/misago/admin/index.html:38
+#, python-format
+msgid ""
+"Your MISAGO_ADDRESS is set to %(configured_address)s while correct value "
+"appears to be %(correct_address)s."
+msgstr ""
+
+#: templates/misago/admin/index.html:42 templates/misago/admin/index.html:47
+msgid ""
+"Misago uses this setting to build correct links in e-mails sent to site "
+"users."
+msgstr ""
+
+#: templates/misago/admin/index.html:46
+msgid "The settings.py is missing MISAGO_ADDRESS value."
+msgstr ""
+
+#: templates/misago/admin/index.html:60
 msgid "Misago version"
 msgstr "论坛框架版本"
 
-#: templates/misago/admin/index.html:56
+#: templates/misago/admin/index.html:83
 msgid "Check version"
 msgstr "检查版本"
 
-#: templates/misago/admin/index.html:62
+#: templates/misago/admin/index.html:89
 msgid "This feature requires \"packaging\" python module."
 msgstr "此功能需要\"packaging\" Python模块"
 
-#: templates/misago/admin/index.html:76
+#: templates/misago/admin/index.html:103
 msgid "DB Contents"
 msgstr "数据库内容"
 
-#: templates/misago/admin/index.html:87
+#: templates/misago/admin/index.html:114
 #: templates/misago/admin/users/delete.html:36
-#: templates/misago/admin/users/list.html:23
+#: templates/misago/admin/users/list.html:24
 #: templates/misago/profile/posts.html:5 templates/misago/profile/posts.html:8
 #: threads/migrations/0002_threads_settings.py:48
-#: threads/migrations/0004_update_settings.py:48 users/apps.py:83
+#: threads/migrations/0004_update_settings.py:48 users/apps.py:91
 msgid "Posts"
 msgstr "发帖"
 
-#: templates/misago/admin/index.html:91 templates/misago/navbar.html:40
+#: templates/misago/admin/index.html:118 templates/misago/navbar.html:40
 #: templates/misago/userslists/base.html:5
-#: templates/misago/userslists/base.html:14 users/admin.py:70
+#: templates/misago/userslists/base.html:14 users/admin.py:80
 #: users/migrations/0002_users_settings.py:16
 #: users/migrations/0006_update_settings.py:17 users/search.py:18
 msgid "Users"
 msgstr "用户"
 
-#: templates/misago/admin/index.html:96
+#: templates/misago/admin/index.html:123
 msgid "Inactive users"
 msgstr "冻结用户"
 
-#: templates/misago/admin/index.html:122
+#: templates/misago/admin/index.html:149
 msgid "Checking..."
 msgstr "检查中…"
 
@@ -1373,7 +1644,7 @@ msgstr "請再試一次。"
 msgid "Username or e-mail"
 msgstr "用户名或电子邮件"
 
-#: templates/misago/admin/login.html:62 users/forms/admin.py:60
+#: templates/misago/admin/login.html:62 users/forms/admin.py:62
 #: users/forms/auth.py:59
 msgid "Password"
 msgstr "密码"
@@ -1418,17 +1689,13 @@ msgid "Display and visibility"
 msgstr "显示和可见性"
 
 #: templates/misago/admin/ranks/list.html:16
-#: templates/misago/admin/users/list.html:21
+#: templates/misago/admin/users/list.html:22
 #: templates/misago/userslists/active_posters.html:95
 #: templates/misago/userslists/active_posters.html:106
-#: users/forms/admin.py:236
+#: users/forms/admin.py:238
 msgid "Rank"
 msgstr "级别"
 
-#: templates/misago/admin/ranks/list.html:17
-msgid "Title"
-msgstr "标题"
-
 #: templates/misago/admin/ranks/list.html:18
 msgid "Special"
 msgstr "特别"
@@ -1470,7 +1737,7 @@ msgid "No user roles are currently defined."
 msgstr "目前没有定义用户身份。"
 
 #: templates/misago/admin/users/ban.html:6
-#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:72
+#: templates/misago/admin/users/ban.html:11 users/views/admin/users.py:73
 msgid "Ban users"
 msgstr "封禁用户"
 
@@ -1478,14 +1745,11 @@ msgstr "封禁用户"
 msgid "Ban selected users:"
 msgstr "封禁所选用户:"
 
-#: templates/misago/admin/users/ban.html:35
-#: templates/misago/admin/users/edit.html:113
-#: templates/misago/admin/users/list.html:34
-#: templates/misago/userslists/active_posters.html:66
-msgid "Avatar"
-msgstr "头像"
+#: templates/misago/admin/users/ban.html:48
+msgid "IP not available"
+msgstr ""
 
-#: templates/misago/admin/users/ban.html:72
+#: templates/misago/admin/users/ban.html:76
 msgid "Set bans"
 msgstr "设置封禁"
 
@@ -1580,6 +1844,18 @@ msgstr ""
 msgid "No staff message is available."
 msgstr "没有可用的工作人员消息。"
 
+#: templates/misago/admin/users/edit.html:220
+msgid "Agreement"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:221
+msgid "Accepted on"
+msgstr ""
+
+#: templates/misago/admin/users/edit.html:239
+msgid "This user didn't accept any agreements."
+msgstr ""
+
 #: templates/misago/admin/users/list.html:9
 #: templates/misago/admin/users/new.html:6
 #: templates/misago/admin/users/new.html:11
@@ -1587,51 +1863,55 @@ msgstr "没有可用的工作人员消息。"
 msgid "New user"
 msgstr "新用户"
 
-#: templates/misago/admin/users/list.html:17
-msgid "User"
-msgstr "用户"
-
-#: templates/misago/admin/users/list.html:20
+#: templates/misago/admin/users/list.html:20 users/signals.py:30
 msgid "E-mail"
 msgstr "电子邮件"
 
-#: templates/misago/admin/users/list.html:22
+#: templates/misago/admin/users/list.html:21
+msgid "IP Address"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:23
 msgid "Joined"
 msgstr "加入"
 
-#: templates/misago/admin/users/list.html:39
+#: templates/misago/admin/users/list.html:40
 msgid "Is deleting their account"
 msgstr ""
 
-#: templates/misago/admin/users/list.html:43
+#: templates/misago/admin/users/list.html:44
 msgid "Is disabled by administrator"
 msgstr "被管理员禁用"
 
-#: templates/misago/admin/users/list.html:54
+#: templates/misago/admin/users/list.html:55
 msgid "Requires activation by administrator"
 msgstr "需要管理员激活"
 
-#: templates/misago/admin/users/list.html:56
+#: templates/misago/admin/users/list.html:57
 msgid "Has to activate account"
 msgstr "必须激活账号"
 
-#: templates/misago/admin/users/list.html:63
+#: templates/misago/admin/users/list.html:64
 msgid "Super administrator"
 msgstr "超级管理员"
 
-#: templates/misago/admin/users/list.html:65
+#: templates/misago/admin/users/list.html:66
 msgid "Administrator"
 msgstr "管理员"
 
-#: templates/misago/admin/users/list.html:90
+#: templates/misago/admin/users/list.html:78
+msgid "IP removed"
+msgstr ""
+
+#: templates/misago/admin/users/list.html:98
 msgid "Edit user"
 msgstr "编辑用户"
 
-#: templates/misago/admin/users/list.html:99
+#: templates/misago/admin/users/list.html:107
 msgid "No users matching search criteria have been found."
 msgstr "没有找到匹配搜索条件的用户。"
 
-#: templates/misago/admin/users/list.html:105
+#: templates/misago/admin/users/list.html:113
 msgid "Search users"
 msgstr "搜索用户"
 
@@ -1835,6 +2115,29 @@ msgstr "要更改您的账号密码,请单击下面的链接:"
 msgid "Set new password"
 msgstr "设置新密码"
 
+#: templates/misago/emails/data_download.html:6
+#: templates/misago/emails/data_download.txt:6
+#, python-format
+msgid ""
+"%(user)s, you are receiving this message because your data is ready for "
+"download."
+msgstr ""
+
+#: templates/misago/emails/data_download.html:14
+#: templates/misago/emails/data_download.txt:15
+#, python-format
+msgid ""
+"This link will remain active for %(expires_in)s hour from the time this "
+"message has been sent."
+msgid_plural ""
+"This link will remain active for %(expires_in)s hours from the time this "
+"message has been sent."
+msgstr[0] ""
+
+#: templates/misago/emails/data_download.txt:10
+msgid "To download your data, click the following link:"
+msgstr ""
+
 #: templates/misago/emails/privatethread/added.html:9
 #, python-format
 msgid ""
@@ -1920,9 +2223,9 @@ msgstr "您的账号被激活后,您可以通过以下链接登录。"
 #: templates/misago/emails/thread/reply.html:9
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread %(thread)s that you are subscribed to."
-msgstr "%(user)s您收到这个消息,因为%(poster)s回复了您订阅的主题帖%(thread)s。"
+msgstr ""
 
 #: templates/misago/emails/thread/reply.html:14
 #: templates/misago/emails/thread/reply.txt:10
@@ -1936,9 +2239,9 @@ msgstr "去回帖"
 #: templates/misago/emails/thread/reply.txt:6
 #, python-format
 msgid ""
-"%(user)s, you are receiving this message because %(poster)s has replied to "
+"%(user)s, you are receiving this message because %(sender)s has replied to "
 "the thread \"%(thread)s\" that you are subscribed to."
-msgstr "%(user)s,您收到这个消息,因为%(poster)s回复了您订阅的主题帖\"%(thread)s\"。"
+msgstr ""
 
 #: templates/misago/errorpages/403.html:40
 msgid "This page is not available."
@@ -2163,7 +2466,7 @@ msgstr "查看结果"
 
 #: templates/misago/profile/ban_details.html:5
 #: templates/misago/profile/ban_details.html:8
-#: templates/misago/profile/ban_details.html:15 users/apps.py:120
+#: templates/misago/profile/ban_details.html:15 users/apps.py:128
 msgid "Ban details"
 msgstr "封禁细节"
 
@@ -2208,7 +2511,7 @@ msgstr[0] "已发起 %(threads)s 主题帖。"
 
 #: templates/misago/profile/details.html:5
 #: templates/misago/profile/details.html:8
-#: templates/misago/profile/details.html:18 users/apps.py:107
+#: templates/misago/profile/details.html:18 users/apps.py:115
 msgid "Details"
 msgstr "详情"
 
@@ -2236,7 +2539,7 @@ msgid "This error is caused by invalid post content manipulation."
 msgstr "此错误是由无效的帖子内容操作引起的。"
 
 #: templates/misago/profile/followers.html:5
-#: templates/misago/profile/followers.html:8 users/apps.py:95
+#: templates/misago/profile/followers.html:8 users/apps.py:103
 msgid "Followers"
 msgstr "关注者"
 
@@ -2262,7 +2565,7 @@ msgid "%(username)s has no followers."
 msgstr "%(username)s没有关注者。"
 
 #: templates/misago/profile/follows.html:5
-#: templates/misago/profile/follows.html:8 users/apps.py:101
+#: templates/misago/profile/follows.html:8 users/apps.py:109
 msgid "Follows"
 msgstr "关注"
 
@@ -2343,7 +2646,7 @@ msgid "%(username)s started no threads."
 msgstr "%(username)s没有发起任何主题帖。"
 
 #: templates/misago/profile/username_history.html:5
-#: templates/misago/profile/username_history.html:8 users/apps.py:113
+#: templates/misago/profile/username_history.html:8 users/apps.py:121
 msgid "Username history"
 msgstr "用户名历史记录"
 
@@ -2368,6 +2671,20 @@ msgstr "您的用户名从未更改。"
 msgid "%(username)s's username was never changed."
 msgstr "%(username)s的用户名从未改变。"
 
+#: templates/misago/required_agreement.html:9
+#, python-format
+msgid "Please review the updated %(agreement)s:"
+msgstr ""
+
+#: templates/misago/required_agreement.html:19
+msgid "here"
+msgstr ""
+
+#: templates/misago/required_agreement.html:21
+#, python-format
+msgid "Please review the updated %(agreement)s available %(link)s."
+msgstr ""
+
 #: templates/misago/search.html:5 templates/misago/search.html:8
 msgid "Search site"
 msgstr "搜索网站"
@@ -2483,10 +2800,6 @@ msgstr "被%(hidden_by)s隐藏于%(hidden_on)s。"
 msgid "By %(event_by)s on %(event_on)s."
 msgstr "被%(event_by)s于%(event_on)s."
 
-#: templates/misago/thread/posts/event/info.html:34
-msgid "IP recorded"
-msgstr "IP记录"
-
 #: templates/misago/thread/posts/post/attachments.html:33
 #, python-format
 msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s on %(uploaded_on)s."
@@ -2608,7 +2921,7 @@ msgstr[0] "%(replies)s条回帖,最后一条于 %(last_post_on)s。"
 
 #: templates/misago/thread/thread.html:24
 msgid "Answered."
-msgstr ""
+msgstr "已回答。"
 
 #: templates/misago/thread/thread.html:24
 msgid "Closed."
@@ -2632,7 +2945,7 @@ msgstr ""
 
 #: templates/misago/thread/toolbar-top.html:26
 msgid "Best answer"
-msgstr ""
+msgstr "最佳答案"
 
 #: templates/misago/threadslist/base.html:63
 msgid "There are no threads in this category."
@@ -2693,7 +3006,7 @@ msgstr "新职位"
 
 #: templates/misago/threadslist/thread.html:59
 msgid "Answered"
-msgstr ""
+msgstr "已回答"
 
 #: templates/misago/threadslist/thread.html:139
 msgid "Poll"
@@ -2760,11 +3073,11 @@ msgstr "附件"
 msgid "Attachment types"
 msgstr "附件类型"
 
-#: threads/api/attachments.py:19
+#: threads/api/attachments.py:20
 msgid "You don't have permission to upload new files."
 msgstr "您没有上传新文件的权限。"
 
-#: threads/api/attachments.py:29
+#: threads/api/attachments.py:30
 msgid "No file has been uploaded."
 msgstr "没有上传文件。"
 
@@ -2772,24 +3085,24 @@ msgstr "没有上传文件。"
 msgid "Uploaded image was corrupted or invalid."
 msgstr "上传的图像已损坏或无效。"
 
-#: threads/api/attachments.py:81
+#: threads/api/attachments.py:83
 msgid "You can't upload files of this type."
 msgstr "您无法上传此类型的文件。"
 
-#: threads/api/attachments.py:86
+#: threads/api/attachments.py:88
 #, python-format
 msgid ""
 "You can't upload files larger than %(limit)s (your file has %(upload)s)."
 msgstr "您不能上传大于%(limit)s的文件(您的文件有%(upload)s)。"
 
-#: threads/api/attachments.py:96
+#: threads/api/attachments.py:98
 #, python-format
 msgid ""
 "You can't upload files of this type larger than %(limit)s (your file has "
 "%(upload)s)."
 msgstr "您不能上传大于%(limit)s的此类型的文件(您的文件有%(upload)s)。"
 
-#: threads/api/postendpoints/edits.py:89
+#: threads/api/postendpoints/edits.py:88
 msgid "Edits record is unavailable for this post."
 msgstr "此帖子无法编辑记录。"
 
@@ -2805,12 +3118,12 @@ msgstr "您不能在此主题帖中移动帖子。"
 msgid "You can't like posts in this category."
 msgstr "您不能赞这个版块的帖子。"
 
-#: threads/api/postendpoints/patch_post.py:108
+#: threads/api/postendpoints/patch_post.py:107
 #: threads/api/threadendpoints/patch.py:130
 msgid "Content approval can't be reversed."
 msgstr "内容审批不能撤销。"
 
-#: threads/api/postendpoints/patch_post.py:186
+#: threads/api/postendpoints/patch_post.py:185
 msgid "One or more posts to update could not be found."
 msgstr "无法找到一条或多条要更新的帖子。"
 
@@ -2823,7 +3136,7 @@ msgstr "您不能从这条主题帖拆分帖子。"
 msgid "You don't have permission to remove \"%(attachment)s\" attachment."
 msgstr "您没有权限删除 \"%(attachment)s\"附件。"
 
-#: threads/api/postingendpoint/attachments.py:128
+#: threads/api/postingendpoint/attachments.py:127
 #, python-format
 msgid ""
 "You can't attach more than %(limit_value)s file to single post (added "
@@ -2893,11 +3206,11 @@ msgstr[0] "您不能再添加%(users)s用户到私人主题帖(您已经添加
 msgid "One or more users could not be found: %(usernames)s"
 msgstr "找不到用户:%(usernames)s"
 
-#: threads/api/postingendpoint/reply.py:81 threads/validators.py:80
+#: threads/api/postingendpoint/reply.py:83 threads/validators.py:80
 msgid "You have to enter a message."
 msgstr "您必须输入消息。"
 
-#: threads/api/postingendpoint/reply.py:103 threads/validators.py:41
+#: threads/api/postingendpoint/reply.py:105 threads/validators.py:41
 msgid "You have to enter thread title."
 msgstr "您必须输入主题帖标题。"
 
@@ -2912,7 +3225,7 @@ msgstr "目前没有允许新主题帖的版块可供您使用。"
 
 #: threads/api/threadendpoints/patch.py:54
 msgid "Not a valid string."
-msgstr ""
+msgstr "不是有效的字符串。"
 
 #: threads/api/threadendpoints/patch.py:74
 msgid "You can't change globally pinned threads weights in this category."
@@ -2939,17 +3252,17 @@ msgstr "您没有权限打开此主题帖。"
 #: threads/api/threadendpoints/patch.py:301
 #: threads/api/threadendpoints/patch.py:331
 msgid "A valid integer is required."
-msgstr ""
+msgstr "需要一个有效的整数。"
 
 #: threads/api/threadendpoints/patch.py:219
 msgid "This post is already marked as thread's best answer."
-msgstr ""
+msgstr "这个帖子已被标记为该主题的最佳答案。"
 
 #: threads/api/threadendpoints/patch.py:252
 msgid ""
 "This post can't be unmarked because it's not currently marked as best "
 "answer."
-msgstr ""
+msgstr "这篇文章不能被取消标记,因为它目前没有被标记为最佳答案。"
 
 #: threads/api/threadendpoints/patch.py:277
 msgid "You have to enter new participant's username."
@@ -2974,9 +3287,9 @@ msgstr "该用户已经是主题帖所有者。"
 
 #: threads/api/threadendpoints/patch.py:447
 msgid "One or more threads to update could not be found."
-msgstr ""
+msgstr "找不到一个或多个要更新的线程。"
 
-#: threads/api/threadpoll.py:53
+#: threads/api/threadpoll.py:54
 msgid "There's already a poll in this thread."
 msgstr "此主题帖已存在一个调查。"
 
@@ -3012,7 +3325,7 @@ msgstr "文件名包含"
 msgid "File type"
 msgstr "文件类型"
 
-#: threads/forms.py:24 users/forms/admin.py:595
+#: threads/forms.py:24 users/forms/admin.py:609
 msgid "State"
 msgstr "状态"
 
@@ -3036,10 +3349,6 @@ msgstr "文件扩展名"
 msgid "Maximum allowed uploaded file size"
 msgstr "最大允许上传的大小"
 
-#: threads/forms.py:56
-msgid "Status"
-msgstr "状态"
-
 #: threads/forms.py:57
 msgid "Limit uploads to"
 msgstr "将上传权限限制于"
@@ -3090,7 +3399,7 @@ msgstr ""
 
 #: threads/mergeconflict.py:57
 msgid "Unmark all best answers"
-msgstr ""
+msgstr "取消标记所有最佳答案"
 
 #: threads/mergeconflict.py:76
 msgid "Delete all polls"
@@ -3218,7 +3527,7 @@ msgstr "可以下载附件"
 
 #: threads/permissions/bestanswers.py:29
 msgid "Best answers"
-msgstr ""
+msgstr "最佳答案"
 
 #: threads/permissions/bestanswers.py:32
 msgid "Can mark posts as best answers"
@@ -3292,13 +3601,13 @@ msgstr ""
 msgid ""
 "You don't have permission to change this thread's marked answer because you "
 "are not a thread starter."
-msgstr ""
+msgstr "因为您不是主题发起人,所有您无权更改此主题的最佳答案。"
 
 #: threads/permissions/bestanswers.py:221
 msgid ""
 "You don't have permission to change this thread's best answer because a "
 "moderator has protected it."
-msgstr ""
+msgstr "您没有权限更改此主题的最佳答案,因为主持人已对其进行了保护。"
 
 #: threads/permissions/bestanswers.py:232
 msgid "You have to sign in to unmark best answers."
@@ -3648,7 +3957,7 @@ msgstr "允许比洪水攻击保护更频繁的发帖。"
 msgid "Can see threads"
 msgstr "可以看到主题帖"
 
-#: threads/permissions/threads.py:110 users/forms/admin.py:166
+#: threads/permissions/threads.py:110 users/forms/admin.py:168
 #: users/migrations/0002_users_settings.py:144
 #: users/migrations/0006_update_settings.py:130
 msgid "Started threads"
@@ -4525,6 +4834,14 @@ msgstr "存在不合法投票选项。"
 msgid "You have to make a choice."
 msgstr "您必须做出选择。"
 
+#: threads/signals.py:177
+msgid "Question"
+msgstr ""
+
+#: threads/signals.py:178
+msgid "Choices"
+msgstr ""
+
 #: threads/templatetags/misago_poststags.py:20
 #, python-format
 msgid "%(user)s likes this."
@@ -4654,23 +4971,13 @@ msgstr "您没有权限查看未批准的内容列表。"
 msgid "Requested attachment could not be found."
 msgstr "无法找到请求的附件。"
 
-#: threads/views/admin/attachments.py:24 users/views/admin/bans.py:24
-#: users/views/admin/users.py:55
-msgid "From newest"
-msgstr "从最新注册的"
-
-#: threads/views/admin/attachments.py:25 users/views/admin/bans.py:25
-#: users/views/admin/users.py:56
-msgid "From oldest"
-msgstr "从最早注册的"
-
 #: threads/views/admin/attachments.py:26 users/views/admin/bans.py:26
-#: users/views/admin/users.py:57
+#: users/views/admin/users.py:58
 msgid "A to z"
 msgstr "用户名A到z"
 
 #: threads/views/admin/attachments.py:27 users/views/admin/bans.py:27
-#: users/views/admin/users.py:58
+#: users/views/admin/users.py:59
 msgid "Z to a"
 msgstr "用户名Z到a"
 
@@ -4739,33 +5046,37 @@ msgid ""
 "post."
 msgstr "您必须有批准内容的权限才能前往第一条未批准的帖子。"
 
-#: users/admin.py:79
+#: users/admin.py:89
 msgid "User Accounts"
 msgstr "用户账号"
 
-#: users/admin.py:87
+#: users/admin.py:97
 msgid "Ranks"
 msgstr "级别"
 
-#: users/admin.py:96
+#: users/admin.py:106
 msgid "Bans"
 msgstr "封禁"
 
+#: users/admin.py:115
+msgid "Data downloads"
+msgstr ""
+
 #: users/api/auth.py:100
 #, python-format
 msgid "Activate %(user)s account on %(forum_name)s forums"
 msgstr "在%(forum_name)s论坛启用%(user)s账号"
 
-#: users/api/auth.py:139
+#: users/api/auth.py:138
 #, python-format
 msgid "Change %(user)s password on %(forum_name)s forums"
 msgstr "在%(forum_name)s论坛更改%(user)s密码"
 
-#: users/api/auth.py:180
+#: users/api/auth.py:178
 msgid "Form link is invalid. Please try again."
 msgstr "表单链接无效。请再试一次。"
 
-#: users/api/auth.py:181
+#: users/api/auth.py:179
 msgid "Your link has expired. Please request new one."
 msgstr "您的链接已过期,请申请新的。"
 
@@ -4835,15 +5146,15 @@ msgstr "电子邮件更改确认链接已经发送到新地址。"
 msgid "Confirm password change on %(forum_name)s forums"
 msgstr "确认更改%(forum_name)s论坛的密码"
 
-#: users/api/userendpoints/changepassword.py:31
+#: users/api/userendpoints/changepassword.py:32
 msgid "Password change confirmation link was sent to your address."
 msgstr "密码更改确认链接已经发送到您的地址。"
 
-#: users/api/userendpoints/create.py:22
+#: users/api/userendpoints/create.py:25
 msgid "New users registrations are currently closed."
 msgstr "新用户注册目前已关闭。"
 
-#: users/api/userendpoints/create.py:53 users/social/pipeline.py:205
+#: users/api/userendpoints/create.py:61 users/social/pipeline.py:215
 msgid "Please try resubmitting the form."
 msgstr "请重新尝试提交表单"
 
@@ -4868,38 +5179,54 @@ msgstr "更改用户名时出错,请再试一次。"
 msgid "You don't have permission to see other users name history."
 msgstr "您没有权限查看其他用户名称历史记录。"
 
-#: users/api/users.py:54
+#: users/api/users.py:57
 msgid "You have to sign in to perform this action."
 msgstr "您必须登录才能执行此操作。"
 
-#: users/api/users.py:100
+#: users/api/users.py:103
 msgid "You can't change other users avatars."
 msgstr "您不能改变其他用户的头像。 "
 
-#: users/api/users.py:107
+#: users/api/users.py:110
 msgid "You can't change other users options."
 msgstr "您不能更改其他用户选项。"
 
-#: users/api/users.py:112
+#: users/api/users.py:115
 msgid "Your forum options have been changed."
 msgstr "您的论坛设置已更改。"
 
-#: users/api/users.py:119
+#: users/api/users.py:122
 msgid "You can't change other users names."
 msgstr "您不能更改其他用户名。"
 
-#: users/api/users.py:126
+#: users/api/users.py:129
 msgid "You can't change other users signatures."
 msgstr "您不能更改其他用户签名。"
 
-#: users/api/users.py:133
+#: users/api/users.py:136
 msgid "You can't change other users passwords."
 msgstr "您不能更改其他用户的密码。"
 
-#: users/api/users.py:140
+#: users/api/users.py:143
 msgid "You can't change other users e-mail addresses."
 msgstr "您不能更改其他用户的电子邮件地址。"
 
+#: users/api/users.py:225
+msgid "You can't request data downloads for other users."
+msgstr ""
+
+#: users/api/users.py:228
+msgid "You can't download your data."
+msgstr ""
+
+#: users/api/users.py:232
+msgid "You can't have more than one data download request at single time."
+msgstr ""
+
+#: users/api/users.py:278
+msgid "You can't see other users data downloads."
+msgstr ""
+
 #: users/apps.py:30
 msgid "Edit details"
 msgstr "编辑详情"
@@ -4912,11 +5239,11 @@ msgstr "更改用户名"
 msgid "Change email or password"
 msgstr "更改电子邮件或密码"
 
-#: users/apps.py:50
+#: users/apps.py:58
 msgid "Delete account"
 msgstr ""
 
-#: users/apps.py:59
+#: users/apps.py:67
 msgid "Active poster"
 msgstr ""
 
@@ -4973,294 +5300,294 @@ msgstr "编辑权限和组"
 msgid "Edit the user from Misago admin panel"
 msgstr "在论坛框架管理面版编辑用户"
 
-#: users/forms/admin.py:21 users/models/ban.py:74
+#: users/forms/admin.py:23 users/models/ban.py:74 users/signals.py:29
 msgid "Username"
 msgstr "用户名"
 
-#: users/forms/admin.py:22
+#: users/forms/admin.py:24
 msgid "Custom title"
 msgstr "自定义标题"
 
-#: users/forms/admin.py:23 users/models/ban.py:75
+#: users/forms/admin.py:25 users/models/ban.py:75
 msgid "E-mail address"
 msgstr "电子邮件地址"
 
-#: users/forms/admin.py:52
+#: users/forms/admin.py:54
 msgid "All registered members must have \"Member\" role."
 msgstr "所有注册的成员都必须具有“论坛成员”身份。"
 
-#: users/forms/admin.py:71
+#: users/forms/admin.py:73
 msgid "Is administrator"
 msgstr "是管理员"
 
-#: users/forms/admin.py:73
+#: users/forms/admin.py:75
 msgid ""
 "Designates whether the user can log into admin sites. If Django admin site "
 "is enabled, this user will need additional permissions assigned within it to"
 " admin Django modules."
 msgstr "指定用户是否可以登录到管理员站点。如果Django管理站点启用,用户将需要额外的Django模块的权限。"
 
-#: users/forms/admin.py:79
+#: users/forms/admin.py:81
 msgid "Is superuser"
 msgstr "是超级用户"
 
-#: users/forms/admin.py:81
+#: users/forms/admin.py:83
 msgid ""
 "Only administrators can access admin sites. In addition to admin site "
 "access, superadmins can also change other members admin levels."
 msgstr "只有管理员可以访问管理页面。另外,超级管理员可以改变其他成员的管理权限。"
 
-#: users/forms/admin.py:86
+#: users/forms/admin.py:88
 msgid "Is active"
 msgstr "未冻结"
 
-#: users/forms/admin.py:88
+#: users/forms/admin.py:90
 msgid ""
 "Designates whether this user should be treated as active. Turning this off "
 "is non-destructible way to remove user accounts."
 msgstr "指定该用户是否应被视为未冻结状态。当需要删除用户账号时将它设为“否”来冻结用户账号即可保留数据。"
 
-#: users/forms/admin.py:92 users/forms/admin.py:123 users/forms/admin.py:151
+#: users/forms/admin.py:94 users/forms/admin.py:125 users/forms/admin.py:153
 msgid "Staff message"
 msgstr "员工可见消息"
 
-#: users/forms/admin.py:94
+#: users/forms/admin.py:96
 msgid ""
 "Optional message for forum team members explaining why user's account has "
 "been disabled."
 msgstr "呈现给论坛管理团队的说明用户账号被封禁原因的消息。(可选)"
 
-#: users/forms/admin.py:99
+#: users/forms/admin.py:101
 msgid "Change password to"
 msgstr "更改密码"
 
-#: users/forms/admin.py:106
+#: users/forms/admin.py:108
 msgid "Lock avatar"
 msgstr "锁头像"
 
-#: users/forms/admin.py:108
+#: users/forms/admin.py:110
 msgid ""
 "Setting this to yes will stop user from changing his/her avatar, and will "
 "reset his/her avatar to procedurally generated one."
 msgstr "设置为“是”将阻止用户更改其头像,并将会重置其头像(按照程序生成一个)。"
 
-#: users/forms/admin.py:114 users/forms/admin.py:145 users/forms/admin.py:473
-#: users/forms/admin.py:526
+#: users/forms/admin.py:116 users/forms/admin.py:147 users/forms/admin.py:468
+#: users/forms/admin.py:540
 msgid "User message"
 msgstr "用户可见消息"
 
-#: users/forms/admin.py:116
+#: users/forms/admin.py:118
 msgid ""
 "Optional message for user explaining why he/she is banned form changing "
 "avatar."
 msgstr "用于向用户说明他被禁止更改头像的原因的消息(可选)。"
 
-#: users/forms/admin.py:125
+#: users/forms/admin.py:127
 msgid ""
 "Optional message for forum team members explaining why user is banned form "
 "changing avatar."
 msgstr "呈现给论坛管理团队的用来说明用户被禁止更改头像的原因的消息(可选)。"
 
-#: users/forms/admin.py:133
+#: users/forms/admin.py:135
 msgid "Signature contents"
 msgstr "签名内容"
 
-#: users/forms/admin.py:138
+#: users/forms/admin.py:140
 msgid "Lock signature"
 msgstr "锁定签名"
 
-#: users/forms/admin.py:140
+#: users/forms/admin.py:142
 msgid ""
 "Setting this to yes will stop user from making changes to his/her signature."
 msgstr "将此设置为“是”将阻止用户更改其签名。"
 
-#: users/forms/admin.py:146
+#: users/forms/admin.py:148
 msgid "Optional message to user explaining why his/hers signature is locked."
 msgstr "用来向用户说明其签名被锁定原因的消息。(可选)"
 
-#: users/forms/admin.py:152
+#: users/forms/admin.py:154
 msgid ""
 "Optional message to team members explaining why user signature is locked."
 msgstr "呈现给论坛管理团队的说明用户签名被锁定原因的消息。(可选)"
 
-#: users/forms/admin.py:157
+#: users/forms/admin.py:159
 msgid "Hides presence"
 msgstr "隐藏存在"
 
-#: users/forms/admin.py:160
+#: users/forms/admin.py:162
 msgid "Who can add user to private threads"
 msgstr "谁可以将用户添加到私有主题帖"
 
-#: users/forms/admin.py:169
+#: users/forms/admin.py:171
 msgid "Replid threads"
 msgstr "回复的主题帖"
 
-#: users/forms/admin.py:219 users/serializers/moderation.py:42
+#: users/forms/admin.py:221 users/serializers/moderation.py:42
 #, python-format
 msgid "Signature can't be longer than %(limit)s character."
 msgid_plural "Signature can't be longer than %(limit)s characters."
 msgstr[0] "签名不能超过%(limit)s字符。"
 
-#: users/forms/admin.py:238
+#: users/forms/admin.py:240
 msgid ""
 "Ranks are used to group and distinguish users. They are also used to add "
 "permissions to groups of users."
 msgstr "级别用于分组和区分用户。它们也向用户组添加权限。"
 
-#: users/forms/admin.py:248
+#: users/forms/admin.py:250
 msgid "Roles"
 msgstr "身份"
 
-#: users/forms/admin.py:249
+#: users/forms/admin.py:251
 msgid "Individual roles of this user. All users must have \"member\" role."
 msgstr "这个用户的个人身份。所有用户都必须有“论坛成员”的身份。"
 
-#: users/forms/admin.py:307
+#: users/forms/admin.py:309
 msgid "Username starts with"
 msgstr "用户名前几位"
 
-#: users/forms/admin.py:308
+#: users/forms/admin.py:310
 msgid "E-mail starts with"
 msgstr "电子邮件前几位"
 
-#: users/forms/admin.py:309
+#: users/forms/admin.py:311
 msgid "Profile fields contain"
 msgstr "个人资料包含"
 
-#: users/forms/admin.py:310
+#: users/forms/admin.py:312
 msgid "Inactive only"
 msgstr "仅限已冻结"
 
-#: users/forms/admin.py:311
+#: users/forms/admin.py:313
 msgid "Disabled only"
 msgstr "仅限关闭"
 
-#: users/forms/admin.py:312
+#: users/forms/admin.py:314
 msgid "Admins only"
 msgstr "仅限管理员"
 
-#: users/forms/admin.py:313
+#: users/forms/admin.py:315
 msgid "Deleting their accounts"
 msgstr ""
 
-#: users/forms/admin.py:355
+#: users/forms/admin.py:357
 msgid "All ranks"
 msgstr "所有级别"
 
-#: users/forms/admin.py:362
+#: users/forms/admin.py:364
 msgid "All roles"
 msgstr "所有身份"
 
-#: users/forms/admin.py:369
+#: users/forms/admin.py:371
 msgid "Has rank"
 msgstr "具有级别"
 
-#: users/forms/admin.py:375
+#: users/forms/admin.py:377
 msgid "Has role"
 msgstr "拥有身份"
 
-#: users/forms/admin.py:391
+#: users/forms/admin.py:393
 msgid ""
 "Short and descriptive name of all users with this rank. \"The Team\" or "
 "\"Game Masters\" are good examples."
 msgstr "该级别的用户的短的描述性名字。\"团队\"或者\"游戏大师”都是好的例子。"
 
-#: users/forms/admin.py:396
+#: users/forms/admin.py:398
 msgid "User title"
 msgstr "用户名"
 
-#: users/forms/admin.py:399
+#: users/forms/admin.py:401
 msgid ""
 "Optional, singular version of rank name displayed by user names. For example"
 " \"GM\" or \"Dev\"."
 msgstr "通过用户名展示的级别名称。例如“游戏大师”或“开发”(可选)。"
 
-#: users/forms/admin.py:409
+#: users/forms/admin.py:411
 msgid ""
 "Optional description explaining function or status of members distincted "
 "with this rank."
 msgstr "用于解释此级别成员的功能和地位的描述 (可选)"
 
-#: users/forms/admin.py:418
+#: users/forms/admin.py:420
 msgid "Rank can give additional roles to users with it."
 msgstr "级别可以为用户提供额外身份。"
 
-#: users/forms/admin.py:423
+#: users/forms/admin.py:425
 msgid "Optional css class added to content belonging to this rank owner."
 msgstr "可选的css类,添加到该等级的用户的内容中。"
 
-#: users/forms/admin.py:426
+#: users/forms/admin.py:428
 msgid "Give rank dedicated tab on users list"
 msgstr "给用户列表添加级别专用标签"
 
-#: users/forms/admin.py:429
+#: users/forms/admin.py:431
 msgid ""
 "Selecting this option will make users with this rank easily discoverable by "
 "others through dedicated page on forum users list."
 msgstr "选择此选项将使得具有此级别的用户可以轻松地被其他人在论坛用户列表专用页上发现。"
 
-#: users/forms/admin.py:454
+#: users/forms/admin.py:456
 msgid "This name collides with other rank."
 msgstr "与其他级别重名。"
 
-#: users/forms/admin.py:461
+#: users/forms/admin.py:463
 msgid "Values to ban"
 msgstr "封禁的值"
 
-#: users/forms/admin.py:464 users/forms/admin.py:579
+#: users/forms/admin.py:471
+msgid "Optional message displayed to users instead of default one."
+msgstr "用来代替默认消息显示给用户的消息。(可选)"
+
+#: users/forms/admin.py:474 users/forms/admin.py:484 users/forms/admin.py:546
+#: users/forms/admin.py:556
+msgid "Message can't be longer than 1000 characters."
+msgstr "消息不能超过1000个字符。"
+
+#: users/forms/admin.py:478 users/forms/admin.py:550
+msgid "Team message"
+msgstr "团队讯息"
+
+#: users/forms/admin.py:481 users/forms/admin.py:553
+msgid "Optional ban message for moderators and administrators."
+msgstr "呈现给管理员和版主的封禁消息。(可选)"
+
+#: users/forms/admin.py:490
+msgid "Leave this field empty for set bans to never expire."
+msgstr "将此字段留空,以使封禁永不到期。"
+
+#: users/forms/admin.py:499 users/forms/admin.py:593
 msgid "Usernames"
 msgstr "用户名"
 
-#: users/forms/admin.py:465 users/forms/admin.py:580
+#: users/forms/admin.py:500 users/forms/admin.py:594
 msgid "E-mails"
 msgstr "电子邮件"
 
-#: users/forms/admin.py:466
+#: users/forms/admin.py:501
 msgid "E-mail domains"
 msgstr "电子邮件域"
 
-#: users/forms/admin.py:467
+#: users/forms/admin.py:507
 msgid "IP addresses"
 msgstr "IP地址"
 
-#: users/forms/admin.py:468
+#: users/forms/admin.py:508
 msgid "First segment of IP addresses"
 msgstr "IP地址第一段"
 
-#: users/forms/admin.py:469
+#: users/forms/admin.py:509
 msgid "First two segments of IP addresses"
 msgstr "前两个IP地址段"
 
-#: users/forms/admin.py:476
-msgid "Optional message displayed to users instead of default one."
-msgstr "用来代替默认消息显示给用户的消息。(可选)"
-
-#: users/forms/admin.py:479 users/forms/admin.py:489 users/forms/admin.py:532
-#: users/forms/admin.py:542
-msgid "Message can't be longer than 1000 characters."
-msgstr "消息不能超过1000个字符。"
-
-#: users/forms/admin.py:483 users/forms/admin.py:536
-msgid "Team message"
-msgstr "团队讯息"
-
-#: users/forms/admin.py:486 users/forms/admin.py:539
-msgid "Optional ban message for moderators and administrators."
-msgstr "呈现给管理员和版主的封禁消息。(可选)"
-
-#: users/forms/admin.py:495
-msgid "Leave this field empty for set bans to never expire."
-msgstr "将此字段留空,以使封禁永不到期。"
-
-#: users/forms/admin.py:501
+#: users/forms/admin.py:515
 msgid "Check type"
 msgstr "检查类型"
 
-#: users/forms/admin.py:506
+#: users/forms/admin.py:520
 msgid "Restrict this ban to registrations"
 msgstr "此封禁仅限于注册时"
 
-#: users/forms/admin.py:508
+#: users/forms/admin.py:522
 msgid ""
 "Changing this to yes will make this ban check be only performed on "
 "registration step. This is good if you want to block certain registrations "
@@ -5268,61 +5595,84 @@ msgid ""
 "existing users."
 msgstr "设置为“是”可以使得该封禁只在注册步骤生效。例如,这能帮助您阻止来自近期被攻击的电子邮件提供商的用户注册,而不影响现有的用户。"
 
-#: users/forms/admin.py:514
+#: users/forms/admin.py:528
 msgid "Banned value"
 msgstr "被封禁的值"
 
-#: users/forms/admin.py:517
+#: users/forms/admin.py:531
 msgid ""
 "This value is case-insensitive and accepts asterisk (*) for rought matches. "
 "For example, making IP ban for value \"83.*\" will ban all IP addresses "
 "beginning with \"83.\"."
 msgstr "这个值不区分大小写,并接受*作为通配符。例如,\"83.*\"的封禁将会禁止所有以\"83.\"开头的IP地址。"
 
-#: users/forms/admin.py:522
+#: users/forms/admin.py:536
 msgid "Banned value can't be longer than 250 characters."
 msgstr "被封禁的值不能超过250个字符。"
 
-#: users/forms/admin.py:529
+#: users/forms/admin.py:543
 msgid "Optional message displayed to user instead of default one."
 msgstr "代替默认消息显示给用户。(可选)"
 
-#: users/forms/admin.py:548
+#: users/forms/admin.py:562
 msgid "Leave this field empty for this ban to never expire."
 msgstr "不填写此字段,则封禁永不过期。"
 
-#: users/forms/admin.py:568
+#: users/forms/admin.py:582
 msgid "Banned value is too vague."
 msgstr "被封禁的值太含糊。"
 
-#: users/forms/admin.py:578
+#: users/forms/admin.py:592
 msgid "All bans"
 msgstr "所有封禁"
 
-#: users/forms/admin.py:581
+#: users/forms/admin.py:595
 msgid "IPs"
 msgstr "IPs"
 
-#: users/forms/admin.py:584
+#: users/forms/admin.py:598
 msgid "Banned value begins with"
 msgstr "封禁值开头"
 
-#: users/forms/admin.py:586
+#: users/forms/admin.py:600
 msgid "Registration only"
 msgstr "仅限注册"
 
-#: users/forms/admin.py:589 users/forms/admin.py:598
+#: users/forms/admin.py:603 users/forms/admin.py:612
 msgid "Any"
 msgstr "任何"
 
-#: users/forms/admin.py:599
+#: users/forms/admin.py:613
 msgid "Active"
 msgstr "活动的"
 
-#: users/forms/admin.py:600
+#: users/forms/admin.py:614 users/models/datadownload.py:26
 msgid "Expired"
 msgstr "已过期"
 
+#: users/forms/admin.py:649
+msgid "Usernames or emails"
+msgstr ""
+
+#: users/forms/admin.py:651
+msgid ""
+"Enter every item in new line. Duplicates will be ignored. This field is case"
+" insensitive. Depending on site configuration and amount of data to archive "
+"it may take up to few days for requests to complete. E-mail will "
+"notification will be sent to every user once their download is ready."
+msgstr ""
+
+#: users/forms/admin.py:667
+#, python-format
+msgid ""
+"You may not enter more than 20 items at single time (You have entered "
+"%(show_value)s)."
+msgstr ""
+
+#: users/forms/admin.py:684
+msgid "One or more specified users could not be found."
+msgstr ""
+
 #: users/forms/auth.py:16
 msgid "Fill out both fields."
 msgstr "填写两个字段。"
@@ -5379,18 +5729,27 @@ msgid ""
 "request new password."
 msgstr "管理员必须激活您的账号,您才能申请新密码。"
 
-#: users/forms/register.py:27
+#: users/forms/register.py:34
 msgid "This usernane is not allowed."
 msgstr "此用户名不允许。"
 
-#: users/forms/register.py:38 users/validators.py:41
+#: users/forms/register.py:45 users/validators.py:41
 msgid "This e-mail address is not allowed."
 msgstr "该电子邮件地址是不允许的。"
 
-#: users/forms/register.py:47
+#: users/forms/register.py:51
+msgid "This agreement is required."
+msgstr ""
+
+#: users/forms/register.py:60
 msgid "New registrations from this IP address are not allowed."
 msgstr "不允许从该IP地址新注册。"
 
+#: users/management/commands/prepareuserdatadownloads.py:34
+#, python-format
+msgid "%(user)s, your data download is ready"
+msgstr ""
+
 #: users/migrations/0002_users_settings.py:18
 #: users/migrations/0006_update_settings.py:19
 msgid ""
@@ -5614,7 +5973,7 @@ msgid "Enter each answer in new line. Answers are case-insensitive."
 msgstr "在新行中输入每个答案。答案不区分大小写。"
 
 #: users/migrations/0004_default_ranks.py:17
-#: users/migrations/0004_default_ranks.py:18 users/models/user.py:111
+#: users/migrations/0004_default_ranks.py:18 users/models/user.py:113
 msgid "Forum team"
 msgstr "论坛团队"
 
@@ -5627,47 +5986,59 @@ msgstr "团队"
 msgid "Members"
 msgstr "论坛成员"
 
-#: users/models/user.py:37
+#: users/models/datadownload.py:23
+msgid "Pending"
+msgstr ""
+
+#: users/models/datadownload.py:24
+msgid "Processing"
+msgstr ""
+
+#: users/models/datadownload.py:25
+msgid "Ready"
+msgstr ""
+
+#: users/models/user.py:38
 msgid "User must have an email address."
 msgstr "用户必须有一个电子邮件地址。"
 
-#: users/models/user.py:146
+#: users/models/user.py:148
 msgid "Notify"
 msgstr "通知"
 
-#: users/models/user.py:147
+#: users/models/user.py:149
 msgid "Notify with e-mail"
 msgstr "通过电子邮件通知"
 
-#: users/models/user.py:155
+#: users/models/user.py:157
 msgid "Everybody"
 msgstr "大家"
 
-#: users/models/user.py:156
+#: users/models/user.py:158
 msgid "Users I follow"
 msgstr "我关注的用户"
 
-#: users/models/user.py:157
+#: users/models/user.py:159
 msgid "Nobody"
 msgstr "没有人"
 
-#: users/models/user.py:175
+#: users/models/user.py:177
 msgid "joined on"
 msgstr "加入了"
 
-#: users/models/user.py:190
+#: users/models/user.py:191
 msgid "staff status"
 msgstr "员工状态"
 
-#: users/models/user.py:192
+#: users/models/user.py:193
 msgid "Designates whether the user can log into admin sites."
 msgstr "指定用户是否可以登录管理站点。"
 
-#: users/models/user.py:199
+#: users/models/user.py:200
 msgid "active"
 msgstr "未冻结"
 
-#: users/models/user.py:203
+#: users/models/user.py:204
 msgid ""
 "Designates whether this user should be treated as active. Unselect this "
 "instead of deleting accounts."
@@ -6018,11 +6389,7 @@ msgstr ""
 msgid "Join IP"
 msgstr "创建时IP"
 
-#: users/profilefields/default.py:99
-msgid "Last IP"
-msgstr "最后登录IP"
-
-#: users/registration.py:9
+#: users/registration.py:11
 #, python-format
 msgid "Welcome on %(forum_name)s forums!"
 msgstr "欢迎来到%(forum_name)s论坛!"
@@ -6064,14 +6431,30 @@ msgstr "您必须输入新的电子邮件地址。"
 msgid "New e-mail is same as current one."
 msgstr "新电子邮件与当前电子邮件相同。"
 
-#: users/social/pipeline.py:74
+#: users/signals.py:31
+msgid "Joined on"
+msgstr ""
+
+#: users/signals.py:32
+msgid "Joined from ip"
+msgstr ""
+
+#: users/signals.py:72
+msgid "New username"
+msgstr ""
+
+#: users/signals.py:73
+msgid "Old username"
+msgstr ""
+
+#: users/social/pipeline.py:77
 #, python-format
 msgid ""
 "The e-mail address associated with your %(backend)s account is not available"
 " for use on this site."
 msgstr ""
 
-#: users/social/pipeline.py:83
+#: users/social/pipeline.py:86
 #, python-format
 msgid ""
 "Your account has to be activated by site administrator before you will be "
@@ -6165,6 +6548,42 @@ msgstr "封禁\"%(name)s\"已经被编辑。"
 msgid "Ban \"%(name)s\" has been removed."
 msgstr "封禁\"%(name)s\"已经被移除。"
 
+#: users/views/admin/datadownloads.py:23
+msgid "With data downloads: 0"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:24
+msgid "Select data downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:28
+msgid "Expire downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:30
+msgid "Are you sure you want to set selected data downloads as expired?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:34
+msgid "Delete downloads"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:36
+msgid "Are you sure you want to delete selected data downloads?"
+msgstr ""
+
+#: users/views/admin/datadownloads.py:51
+msgid "Selected data downloads have been set as expired."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:57
+msgid "Selected data downloads have been deleted."
+msgstr ""
+
+#: users/views/admin/datadownloads.py:68
+msgid "Data downloads have been requested for specified users."
+msgstr ""
+
 #: users/views/admin/ranks.py:16
 msgid "Requested rank does not exist."
 msgstr "请求的级别不存在。"
@@ -6214,92 +6633,100 @@ msgstr "级别“%(name)s\"已经是默认等级。"
 msgid "Rank \"%(name)s\" has been made default."
 msgstr "级别”%(name)s\"已经被设置成默认。"
 
-#: users/views/admin/users.py:59
+#: users/views/admin/users.py:60
 msgid "Biggest posters"
 msgstr "最少发帖。"
 
-#: users/views/admin/users.py:60
+#: users/views/admin/users.py:61
 msgid "Smallest posters"
 msgstr "最多发帖"
 
-#: users/views/admin/users.py:62
+#: users/views/admin/users.py:63
 msgid "With users: 0"
 msgstr "用户:0"
 
-#: users/views/admin/users.py:63
+#: users/views/admin/users.py:64
 msgid "Select users"
 msgstr "选择用户"
 
-#: users/views/admin/users.py:67
+#: users/views/admin/users.py:68
 msgid "Activate accounts"
 msgstr "激活账号"
 
-#: users/views/admin/users.py:77
+#: users/views/admin/users.py:78
+msgid "Request data download"
+msgstr ""
+
+#: users/views/admin/users.py:83
 msgid "Delete accounts"
 msgstr "删除账号"
 
-#: users/views/admin/users.py:79
+#: users/views/admin/users.py:85
 msgid "Are you sure you want to delete selected users?"
 msgstr "您确定要删除所选用户吗?"
 
-#: users/views/admin/users.py:83
+#: users/views/admin/users.py:89
 msgid "Delete all"
 msgstr "删除所有"
 
-#: users/views/admin/users.py:86
+#: users/views/admin/users.py:92
 msgid ""
 "Are you sure you want to delete selected users? This will also delete all "
 "content associated with their accounts."
 msgstr "您确定要删除所选用户吗?这也会删除所有和此账号关联的内容。"
 
-#: users/views/admin/users.py:107
+#: users/views/admin/users.py:113
 msgid "You have to select inactive users."
 msgstr "您必须选择已冻结用户。"
 
-#: users/views/admin/users.py:114
+#: users/views/admin/users.py:120
 #, python-format
 msgid "Your account on %(forum_name)s forums has been activated"
 msgstr "您在 %(forum_name)s 论坛上的账号已经被激活。"
 
-#: users/views/admin/users.py:119
+#: users/views/admin/users.py:125
 msgid "Selected users accounts have been activated."
 msgstr "选中的用户已经被激活。"
 
-#: users/views/admin/users.py:125
+#: users/views/admin/users.py:131
 #, python-format
 msgid "%(user)s is super admin and can't be banned."
 msgstr "%(user)s是超级管理员,不能被封禁。"
 
-#: users/views/admin/users.py:186
+#: users/views/admin/users.py:194
 msgid "Selected users have been banned."
 msgstr "选中的用户已经被封禁。"
 
-#: users/views/admin/users.py:201 users/views/admin/users.py:215
-#: users/views/admin/users.py:331
+#: users/views/admin/users.py:212
+msgid "Data download requests have been placed for selected users."
+msgstr ""
+
+#: users/views/admin/users.py:217 users/views/admin/users.py:230
+#: users/views/admin/users.py:346
 msgid "You can't delete yourself."
 msgstr "您不能删除自己。"
 
-#: users/views/admin/users.py:203 users/views/admin/users.py:217
-#: users/views/admin/users.py:334
+#: users/views/admin/users.py:219 users/views/admin/users.py:232
+#: users/views/admin/users.py:349
 #, python-format
 msgid "%(user)s is admin and can't be deleted."
 msgstr "%(user)s是管理员,不能被删除。"
 
-#: users/views/admin/users.py:209
+#: users/views/admin/users.py:225
 msgid "Selected users have been deleted."
 msgstr "选中的用户已经被删除"
 
-#: users/views/admin/users.py:232
+#: users/views/admin/users.py:247
 #, python-format
 msgid "New user \"%(user)s\" has been registered."
 msgstr "新用户\"%(user)s\"已经被注册。"
 
-#: users/views/admin/users.py:261
+#: users/views/admin/users.py:276
 #, python-format
 msgid "User \"%(user)s\" has been edited."
 msgstr "用户\"%(user)s\"已经被编辑。"
 
-#: users/views/admin/users.py:328
+#: users/views/admin/users.py:343
 msgid "This action can't be accessed directly."
 msgstr ""
 

BIN
misago/locale/zh_Hans/LC_MESSAGES/djangojs.mo


+ 300 - 222
misago/locale/zh_Hans/LC_MESSAGES/djangojs.po

@@ -8,9 +8,9 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-20 21:20+0200\n"
+"POT-Creation-Date: 2018-08-19 15:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: mayne <dreama9ain@gmail.com>, 2017\n"
+"Last-Translator: cxgreat2014 <fwy1998@gmail.com>, 2018\n"
 "Language-Team: Chinese (China) (https://www.transifex.com/misago/teams/65369/zh_CN/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -39,8 +39,30 @@ msgid "Promise can't be resolved itself"
 msgstr ""
 
 #: static/misago/js/misago.js:1
-msgid "By registering you agree to site's terms and conditions."
-msgstr "通过注册,您同意网站的条款和条件。"
+msgid "the terms of service"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "the privacy policy"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "I have read and accept %(agreement)s."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid ""
+"Declining will result in immediate deactivation and deletion of your "
+"account. This action is not reversible."
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Decline"
+msgstr ""
+
+#: static/misago/js/misago.js:1
+msgid "Accept and continue"
+msgstr ""
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
@@ -48,11 +70,12 @@ msgstr "通过注册,您同意网站的条款和条件。"
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:12
 #: static/misago/js/misago.js:13 static/misago/js/misago.js:14
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Close"
 msgstr "未订阅"
 
-#: static/misago/js/misago.js:1 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:1 static/misago/js/misago.js:6
 msgid "Add participant"
 msgstr "添加参与者"
 
@@ -70,9 +93,10 @@ msgstr "要添加用户"
 
 #: static/misago/js/misago.js:1 static/misago/js/misago.js:2
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:6 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:6 static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Cancel"
 msgstr "取消"
 
@@ -192,8 +216,8 @@ msgid "Generate my individual avatar"
 msgstr "自动生成"
 
 #: static/misago/js/misago.js:2 static/misago/js/misago.js:4
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "Ok"
 msgstr "Ok"
 
@@ -261,39 +285,39 @@ msgstr "插入代码"
 msgid "Emphase selection"
 msgstr "重点选择"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert horizontal ruler"
 msgstr "插入水平尺"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link to image"
 msgstr "输入图像链接"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter image label (optional)"
 msgstr "输入图片标签(可选)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert image"
 msgstr "插入图像"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link address"
 msgstr "输入链接地址"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter link label (optional)"
 msgstr "输入链接标签(可选)"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert link"
 msgstr "插入链接"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Enter quote autor, prefix usernames with @"
 msgstr "输入报价autor,前缀用户名@"
 
-#: static/misago/js/misago.js:2
+#: static/misago/js/misago.js:3
 msgid "Insert quote"
 msgstr "插入报价"
 
@@ -325,7 +349,7 @@ msgstr "撤消删除"
 msgid "Error uploading %(filename)s"
 msgstr "上传错误%(filename)s"
 
-#: static/misago/js/misago.js:3 static/misago/js/misago.js:8
+#: static/misago/js/misago.js:3 static/misago/js/misago.js:9
 msgid "Dismiss"
 msgstr "解散"
 
@@ -342,7 +366,7 @@ msgid "Protected"
 msgstr "受保护"
 
 #: static/misago/js/misago.js:3 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Protect"
 msgstr "保护"
 
@@ -369,7 +393,7 @@ msgstr "(成功)"
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:17
 msgid "Best answer"
-msgstr ""
+msgstr "最佳答案"
 
 #: static/misago/js/misago.js:4
 msgid ""
@@ -377,7 +401,7 @@ msgid ""
 " deleted during the merge."
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:18
 msgid "Poll"
 msgstr "调查"
 
@@ -392,12 +416,13 @@ msgid "Are you sure you want to delete all polls?"
 msgstr ""
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Merge threads"
 msgstr "合并主题帖"
 
 #: static/misago/js/misago.js:4
 msgid "Search returned no results."
-msgstr ""
+msgstr "没有找到任何结果。"
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:14
 msgid "Search"
@@ -416,8 +441,7 @@ msgstr ""
 msgid "%(title)s, joined on %(joined_on)s"
 msgstr ""
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "Change username"
 msgstr "更改用户名"
 
@@ -448,7 +472,7 @@ msgstr[0] "已使用更改次数于%(name_changes_expire)s天后重新可用。"
 msgid "Your new username is same as current one."
 msgstr "您的新用户名与当前用户名相同。"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:4 static/misago/js/misago.js:13
 msgid "New username"
 msgstr "新用户名"
 
@@ -457,15 +481,15 @@ msgid "Your username has been changed successfully."
 msgstr "您的用户名已成功更改。"
 
 #: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change your options"
 msgstr "更改您的选项"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5
 msgid "Enter your password to confirm account deletion."
-msgstr ""
+msgstr "输入您的密码以确认删除您的账户。"
 
-#: static/misago/js/misago.js:4 static/misago/js/misago.js:5
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:13
 msgid "Delete account"
 msgstr "删除帐户"
 
@@ -473,35 +497,82 @@ msgstr "删除帐户"
 msgid ""
 "You are going to delete your account. This action is nonreversible, and will"
 " result in following data being deleted:"
-msgstr ""
+msgstr "您将要删除您的帐户。此操作是不可逆的,并且会导致以下数据被删除:"
 
 #: static/misago/js/misago.js:5
 msgid ""
 "Stored IP addresses associated with content that you have posted will be "
 "deleted."
-msgstr ""
+msgstr "与您发布的内容相关联的IP地址记录将被删除。"
 
 #: static/misago/js/misago.js:5
 msgid ""
 "Your username will become available for other user to rename to or for new "
 "user to register their account with."
-msgstr ""
+msgstr "您的用户名将可供其他用户重新命名,或供新用户注册其帐户。"
 
 #: static/misago/js/misago.js:5
 msgid "Your e-mail will become available for use in new account registration."
-msgstr ""
+msgstr "您的电子邮件将可用于新帐户注册。"
 
 #: static/misago/js/misago.js:5
 msgid ""
 "All your posted content will NOT be deleted, but username associated with it"
 " will be changed to one shared by all deleted accounts."
-msgstr ""
+msgstr "您发布的所有内容都不会被删除,但与其关联的用户名将被更改为由所有已删除帐户共享的用户名。"
 
 #: static/misago/js/misago.js:5
 msgid "Delete my account"
+msgstr "删除我的帐户"
+
+#: static/misago/js/misago.js:5
+msgid "Your request for data download has been registered."
 msgstr ""
 
-#: static/misago/js/misago.js:5 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:5
+msgid "Download your data"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"To download your data from the site, click the \"Request data download\" "
+"button. Depending on amount of data to be archived and number of users "
+"wanting to download their data at same time it may take up to few days for "
+"your download to be prepared. An e-mail with notification will be sent to "
+"you when your data is ready to be downloaded."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid ""
+"The download will only be available for limited amount of time, after which "
+"it will be deleted from the site and marked as expired."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Requested on"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "You have no data downloads."
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Request data download"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is being prepared"
+msgstr ""
+
+#: static/misago/js/misago.js:5
+msgid "Download is expired"
+msgstr ""
+
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:12
 msgid "Your details have been updated."
 msgstr "您的信息已更新"
 
@@ -584,7 +655,7 @@ msgstr "我发起的主题帖"
 msgid "Threads I reply to"
 msgstr "我回复的主题帖"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:5 static/misago/js/misago.js:6
 msgid "Change email or password"
 msgstr "更改电子邮件或密码"
 
@@ -592,11 +663,11 @@ msgstr "更改电子邮件或密码"
 msgid ""
 "You need to set a password for your account to be able to change your "
 "username or email."
-msgstr ""
+msgstr "您需要为您的帐户设置密码才能更改您的用户名或电子邮件地址。"
 
 #: static/misago/js/misago.js:5
 msgid "Set password"
-msgstr ""
+msgstr "设置密码"
 
 #: static/misago/js/misago.js:5 static/misago/js/misago.js:15
 msgid "Fill out all fields."
@@ -634,7 +705,7 @@ msgstr "新密码"
 msgid "Repeat password"
 msgstr "重复输入密码"
 
-#: static/misago/js/misago.js:5
+#: static/misago/js/misago.js:6
 msgid "Change forgotten password"
 msgstr "忘记密码"
 
@@ -814,20 +885,20 @@ msgid "%(votes)s vote."
 msgid_plural "%(votes)s votes."
 msgstr[0] "%(votes)s票"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Votes are public."
 msgstr "投票是公开的。"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s vote, %(proc)s% of total."
 msgid_plural "%(votes)s votes, %(proc)s% of total."
 msgstr[0] "%(votes)s投票,%(proc)s占总数的百分比。"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "Your choice."
 msgstr "您的选择。"
 
-#: static/misago/js/misago.js:6
+#: static/misago/js/misago.js:7
 msgid "%(votes)s user has voted for this choice."
 msgid_plural "%(votes)s users have voted for this choice."
 msgstr[0] "%(votes)s用户已经投票选择了这个选择。"
@@ -844,9 +915,8 @@ msgstr "投票"
 msgid "See votes"
 msgstr "查看票数"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:15
 msgid "Edit"
 msgstr "编辑"
 
@@ -856,8 +926,7 @@ msgid ""
 msgstr "您确定要删除此调查吗?该操作是不可逆的。"
 
 #: static/misago/js/misago.js:7 static/misago/js/misago.js:9
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Delete"
 msgstr "删除"
 
@@ -920,27 +989,27 @@ msgstr "帖子已被恢复到以前的状态。"
 msgid "See previous change"
 msgstr "看到以前的变化"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "By %(edited_by)s %(edited_on)s."
 msgstr " %(edited_by) 于 s%(edited_on)s 编辑。"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This post's contents cannot be displayed."
 msgstr "此帖的内容无法显示。"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "This error is caused by invalid post content manipulation."
 msgstr "此错误是由不合法的帖子内容操作引起的。"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "posted %(posted_on)s"
 msgstr "发布于 %(posted_on)s"
 
-#: static/misago/js/misago.js:7 static/misago/js/misago.js:11
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:11
 msgid "Removed user"
 msgstr "被移除用户"
 
-#: static/misago/js/misago.js:7
+#: static/misago/js/misago.js:8
 msgid "See post"
 msgstr "查看帖子"
 
@@ -961,7 +1030,7 @@ msgstr "没有用户赞这条帖子。"
 msgid "Are you sure you want to discard changes?"
 msgstr "您确定要放弃更改吗?"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "You have to enter a message."
 msgstr "您必须输入消息。"
 
@@ -993,11 +1062,12 @@ msgstr "您确定要丢弃私人主题帖吗?"
 msgid "You have to enter at least one recipient."
 msgstr "您必须至少输入一个收件人。"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:15
 msgid "You have to enter thread title."
 msgstr "您必须输入主题帖标题。"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Your thread has been posted."
 msgstr "您的主题帖已发起。"
 
@@ -1005,12 +1075,13 @@ msgstr "您的主题帖已发起。"
 msgid "Comma separated list of user names, eg.: Danny, Lisa"
 msgstr "逗号分隔的用户名列表,例如:Danny,Lisa"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:19
 msgid "Thread title"
 msgstr "主题帖标题"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:8 static/misago/js/misago.js:9
 msgid "Post thread"
 msgstr "发起主题帖"
 
@@ -1018,35 +1089,35 @@ msgstr "发起主题帖"
 msgid "Are you sure you want to discard thread?"
 msgstr "您确定要丢弃主题帖吗?"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18
 msgid "Closed"
 msgstr "已关闭"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:16
 msgid "Open"
 msgstr "打开"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:21
 msgid "Hidden"
 msgstr "隐藏"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Not hidden"
 msgstr "不隐藏"
 
-#: static/misago/js/misago.js:8
+#: static/misago/js/misago.js:9
 msgid "Unpinned"
 msgstr "取消置顶"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned locally"
 msgstr "版块内置顶"
 
-#: static/misago/js/misago.js:8 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:16
 #: static/misago/js/misago.js:18 static/misago/js/misago.js:19
 msgid "Pinned globally"
@@ -1089,12 +1160,12 @@ msgid_plural ""
 msgstr[0] "发帖内容最多包含%(limit_value)s个字符(现有%(show_value)s)。"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Hide"
 msgstr "隐藏"
 
 #: static/misago/js/misago.js:9 static/misago/js/misago.js:10
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Unhide"
 msgstr "取消隐藏"
 
@@ -1116,10 +1187,6 @@ msgid "By %(event_by)s %(event_on)s."
 msgstr " %(event_by)s 于 %(event_on)s。"
 
 #: static/misago/js/misago.js:9
-msgid "IP recorded"
-msgstr "IP记录"
-
-#: static/misago/js/misago.js:9
 msgid "Thread title has been changed from %(old_title)s."
 msgstr "主题帖标题已更改%(old_title)s。"
 
@@ -1208,23 +1275,23 @@ msgstr "您确定要删除这个帖子?这个操作是不可逆的!"
 msgid "Post has been deleted."
 msgstr "帖子已被删除。"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link to this post:"
 msgstr "该帖的永久链接:"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Permament link"
 msgstr "永久链接"
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Mark as best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9
+#: static/misago/js/misago.js:10
 msgid "Unmark best answer"
 msgstr ""
 
-#: static/misago/js/misago.js:9 static/misago/js/misago.js:10
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:11
 msgid "This post was edited %(edits)s time."
 msgid_plural "This post was edited %(edits)s times."
 msgstr[0] "此帖子被编辑了%(edits)s次"
@@ -1233,17 +1300,15 @@ msgstr[0] "此帖子被编辑了%(edits)s次"
 msgid "Changes history"
 msgstr "改变历史"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Approve"
 msgstr "批准"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Move"
 msgstr "移动"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:10 static/misago/js/misago.js:16
 msgid "Split"
 msgstr "分裂"
 
@@ -1256,6 +1321,7 @@ msgid "Move post"
 msgstr "移动帖子"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You have to enter link to the other thread."
 msgstr "您必须输入到另一个主题帖的链接。"
 
@@ -1305,7 +1371,7 @@ msgid "Close thread"
 msgstr "关闭主题帖"
 
 #: static/misago/js/misago.js:10 static/misago/js/misago.js:16
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19
 msgid "Category"
 msgstr "版块"
 
@@ -1352,32 +1418,32 @@ msgid "%(users)s and %(likes)s other user like this."
 msgid_plural "%(users)s and %(likes)s other users like this."
 msgstr[0] "%(users)s 和 %(likes)s 其他用户赞这个。"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Liked"
 msgstr "赞过"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "Like"
 msgstr "赞"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
 msgid "Reply"
 msgstr "回复"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "New post"
 msgstr "新帖子"
 
-#: static/misago/js/misago.js:10 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:20
 msgid "New"
 msgstr "最新"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "This post is protected and may not be edited."
 msgstr "这篇文章是受保护的,不可以被编辑。"
 
-#: static/misago/js/misago.js:10
+#: static/misago/js/misago.js:11
 msgid "protected"
 msgstr "受保护"
 
@@ -1423,65 +1489,64 @@ msgstr "你没有与其他用户分享任何详情"
 msgid "%(username)s is not sharing any details with others."
 msgstr "%(username)s没有与其他用户分享任何详情"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
 msgid "Details"
 msgstr "详情"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s's details have been updated."
 msgstr "%(username)s的信息已更新"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have no started threads."
 msgstr "您还没有发起任何主题帖。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s started no threads."
 msgstr "%(username)s没有发起主题帖。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have started %(threads)s thread."
 msgid_plural "You have started %(threads)s threads."
 msgstr[0] "您已经发起了%(threads)s 条主题帖。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has started %(threads)s thread."
 msgid_plural "%(username)s has started %(threads)s threads."
 msgstr[0] "%(username)s已发起%(threads)s主题帖。"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:13
 msgid "Loading..."
 msgstr "正在加载..."
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:12 static/misago/js/misago.js:19
 #: static/misago/js/misago.js:20
 msgid "Threads"
 msgstr "主题帖"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted no messages."
 msgstr "您没有发布过任何消息。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s posted no messages."
 msgstr "%(username)s没有留言"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "You have posted %(posts)s message."
 msgid_plural "You have posted %(posts)s messages."
 msgstr[0] "您已经发布过%(posts)s 条消息。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "%(username)s has posted %(posts)s message."
 msgid_plural "%(username)s has posted %(posts)s messages."
 msgstr[0] "%(username)s已发布%(posts)s消息。"
 
-#: static/misago/js/misago.js:11
+#: static/misago/js/misago.js:12
 msgid "Posts"
 msgstr "发帖"
 
-#: static/misago/js/misago.js:11 static/misago/js/misago.js:12
+#: static/misago/js/misago.js:12
 msgid "Show more (%(more)s)"
 msgstr "展示更多 (%(more)s)"
 
@@ -1567,6 +1632,7 @@ msgid "Joined %(joined_on)s"
 msgstr "加入%(joined_on)s"
 
 #: static/misago/js/misago.js:12 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Moderation"
 msgstr "调整"
 
@@ -1620,41 +1686,41 @@ msgstr "呈现给论坛管理团队的用于说明禁止用户改变头像的原
 msgid "Avatar controls"
 msgstr "头像限制"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Username has been changed."
 msgstr "用户名已更改。"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account, threads, posts and other content has been deleted."
 msgstr "%(username)s 的帐号,主题,帖子等内容均已被删除。"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid ""
 "%(username)s's account has been deleted and other content has been hidden."
 msgstr "%(username)s 的帐户已被删除,其他内容均已被隐藏。"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete %(username)s"
 msgstr "删除 %(username)s"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Please wait... (%(countdown)ss)"
 msgstr "请稍候... (%(countdown)ss)"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "User content"
 msgstr "用户内容"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Delete together with user's account"
 msgstr "与用户的帐户一起删除"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Hide after deleting user's account"
 msgstr "删除用户帐户后隐藏"
 
-#: static/misago/js/misago.js:12
+#: static/misago/js/misago.js:13
 msgid "Return to users list"
 msgstr "返回用户列表"
 
@@ -1717,75 +1783,74 @@ msgstr "由于某些错误,目前无法注册。"
 msgid "Register"
 msgstr "注册"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Join with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Or create forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:15
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Username"
 msgstr "用户名"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:17
 #: static/misago/js/misago.js:18
 msgid "E-mail"
 msgstr "电子邮件"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid "Password"
 msgstr "密码"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Register account"
 msgstr "注册账号"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but you need to activate it "
 "before you will be able to sign in."
 msgstr "%(username)s,您的帐户已创建,但您需要激活它之后才可以登录。"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:15
 msgid ""
 "%(username)s, your account has been created but board administrator will "
 "have to activate it before you will be able to sign in."
 msgstr "%(username)s,您的帐户已创建,但必须在管理员激活它之后才可以登录。"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid ""
 "We have sent an e-mail to %(email)s with link that you have to click to "
 "activate your account."
 msgstr "我们已向 %(email)s 发送了一封带有激活链接的电子邮件。"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "We will send an e-mail to %(email)s when this takes place."
 msgstr "届时,我们将向 %(email)s 发送一封电子邮件。"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Registration complete"
 msgstr "注册完成"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:14 static/misago/js/misago.js:25
 msgid "Enter a valid email address."
 msgstr "输入一个有效的电子邮件地址。"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Your e-mail address"
 msgstr "您的电子邮件地址"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Send link"
 msgstr "发送链接"
 
-#: static/misago/js/misago.js:13
+#: static/misago/js/misago.js:14
 msgid "Activation link was sent to %(email)s"
 msgstr "激活链接已发送至 %(email)s"
 
-#: static/misago/js/misago.js:13 static/misago/js/misago.js:14
+#: static/misago/js/misago.js:14
 msgid "Request another link"
 msgstr "重新发送链接"
 
@@ -1850,41 +1915,41 @@ msgstr "没有找到匹配搜索查询的用户。"
 msgid "Enter at least two characters to search users."
 msgstr "输入至少两个字符以搜索用户。"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Fill out both fields."
 msgstr "填写两个字段。"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Activate account"
 msgstr "激活帐号"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Sign in with %(site)s"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Or use your forum account:"
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Username or e-mail"
 msgstr "用户名或电子邮件"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Forgot password?"
 msgstr "忘记密码?"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid ""
-"%(username)s, your account has been created and you has been signed in to "
+"%(username)s, your account has been created and you have been signed in to "
 "it."
 msgstr ""
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Registration completed!"
-msgstr ""
+msgstr "注册完成!"
 
-#: static/misago/js/misago.js:14
+#: static/misago/js/misago.js:15
 msgid "Return to forum index"
 msgstr ""
 
@@ -1893,6 +1958,10 @@ msgid "Sign in with %(backend)s"
 msgstr ""
 
 #: static/misago/js/misago.js:15
+msgid "You need to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:15
 msgid "Your e-mail address has been verified by %(backend)s."
 msgstr ""
 
@@ -1902,7 +1971,7 @@ msgstr ""
 
 #: static/misago/js/misago.js:15
 msgid "E-mail address"
-msgstr ""
+msgstr "电子邮件地址"
 
 #: static/misago/js/misago.js:15
 msgid "Change title"
@@ -1913,7 +1982,7 @@ msgid "Edit title"
 msgstr "编辑标题"
 
 #: static/misago/js/misago.js:15 static/misago/js/misago.js:17
-#: static/misago/js/misago.js:18 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:18 static/misago/js/misago.js:20
 msgid "Unapproved"
 msgstr "未批准"
 
@@ -1921,7 +1990,7 @@ msgstr "未批准"
 msgid "Unapproved posts"
 msgstr "未批准的帖子"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:17
+#: static/misago/js/misago.js:15 static/misago/js/misago.js:18
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgstr[0] "%(replies)s回复"
@@ -1942,19 +2011,19 @@ msgid ""
 "reversible!"
 msgstr "您确定要删除所选的帖子吗?这个动作不可逆的!"
 
-#: static/misago/js/misago.js:15 static/misago/js/misago.js:16
+#: static/misago/js/misago.js:16
 msgid "Merge"
 msgstr "合并"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "Unprotect"
 msgstr "取消保护"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "%(username)s on %(posted_on)s"
 msgstr "%(username)s于%(posted_on)s"
 
-#: static/misago/js/misago.js:15
+#: static/misago/js/misago.js:16
 msgid "One or more posts could not be changed:"
 msgstr "无法更改一条或多条帖子"
 
@@ -2014,37 +2083,37 @@ msgstr "版块内置顶"
 msgid "Unpin"
 msgstr "取消置顶"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Merge thread"
 msgstr "合并主题帖"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been merged with other one."
 msgstr "主题帖已经与其他主题帖合并。"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Link to thread you want to merge with"
 msgstr "链接到您要合并的主题帖"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid ""
 "Merge will delete current thread and move its contents to the thread "
 "specified here."
 msgstr "合并将删除当前主题帖并将其内容移动到这里指定的主题帖"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Move thread"
 msgstr "移动主题帖"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "You can't move this thread at the moment."
 msgstr "您现在不可以移动这个主题帖。"
 
-#: static/misago/js/misago.js:16
+#: static/misago/js/misago.js:17
 msgid "Thread has been moved."
 msgstr "主题帖已被移动。"
 
-#: static/misago/js/misago.js:16 static/misago/js/misago.js:19
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:20
 msgid "New category"
 msgstr "新版块"
 
@@ -2081,15 +2150,15 @@ msgstr "已订阅"
 msgid "Disabled"
 msgstr "未订阅"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Unsubscribe"
 msgstr "取消订阅"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe"
 msgstr "订阅"
 
-#: static/misago/js/misago.js:17 static/misago/js/misago.js:18
+#: static/misago/js/misago.js:17 static/misago/js/misago.js:19
 msgid "Subscribe with e-mail"
 msgstr "订阅电子邮件"
 
@@ -2099,7 +2168,7 @@ msgstr "转到第一个新帖"
 
 #: static/misago/js/misago.js:17
 msgid "Go to best answer"
-msgstr ""
+msgstr "转到最佳答案"
 
 #: static/misago/js/misago.js:17
 msgid "Go to first unapproved post"
@@ -2121,11 +2190,11 @@ msgstr "最后的帖子"
 msgid "Options"
 msgstr "选项"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid "Add poll"
 msgstr "添加调查"
 
-#: static/misago/js/misago.js:17
+#: static/misago/js/misago.js:18
 msgid ""
 "There is %(threads)s new or updated thread. Click this message to show it."
 msgid_plural ""
@@ -2139,13 +2208,13 @@ msgstr "新帖子"
 
 #: static/misago/js/misago.js:18
 msgid "Answered"
-msgstr ""
+msgstr "已回答"
 
 #: static/misago/js/misago.js:18
 msgid "Change subscription"
 msgstr "更改订阅"
 
-#: static/misago/js/misago.js:18
+#: static/misago/js/misago.js:19
 msgid "Start thread"
 msgstr "发起主题帖"
 
@@ -2225,7 +2294,7 @@ msgstr "版块内置顶主题帖"
 msgid "Unpin threads"
 msgstr "取消置顶主题帖"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid "Move threads"
 msgstr "移动主题帖"
 
@@ -2261,7 +2330,7 @@ msgstr "主题帖审核"
 msgid "One or more threads could not be deleted:"
 msgstr "无法删除一条或多条主题贴"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:19 static/misago/js/misago.js:20
 msgid ""
 "You can't move threads because there are no categories you are allowed to "
 "move them to."
@@ -2273,61 +2342,61 @@ msgid ""
 " to it."
 msgstr "您必须有权限在版块中发启主题帖,才可以向相应版块合并主题帖,。"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Selected threads were moved."
 msgstr "所选主题帖已移动。"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid ""
 "You need permission to start threads in category to be able to move threads "
 "to it."
 msgstr "您必须有权限在次版块中开启主题帖,才可以移动主题帖到相应版块,。"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select all"
 msgstr "全选"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Select none"
 msgstr "取消选择"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All"
 msgstr "所有"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "All threads"
 msgstr "所有主题帖"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My"
 msgstr "我的"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "My threads"
 msgstr "我的主题帖"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "New threads"
 msgstr "新主题帖"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread"
 msgstr "未读"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unread threads"
 msgstr "未读主题帖"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed"
 msgstr "订阅"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Subscribed threads"
 msgstr "订阅主题"
 
-#: static/misago/js/misago.js:19
+#: static/misago/js/misago.js:20
 msgid "Unapproved content"
 msgstr "未批准的内容"
 
@@ -2343,7 +2412,8 @@ msgstr "登录或注册来发起或参与讨论。"
 msgid "You have unread private threads!"
 msgstr ""
 
-#: static/misago/js/misago.js:20 static/misago/js/misago.js:22
+#: static/misago/js/misago.js:20 static/misago/js/misago.js:21
+#: static/misago/js/misago.js:23
 msgid "Private threads"
 msgstr "私人主题帖"
 
@@ -2351,67 +2421,67 @@ msgstr "私人主题帖"
 msgid "Are you sure you want to sign out?"
 msgstr "您确定要退出吗?"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "See your profile"
 msgstr "个人档案"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change options"
 msgstr "更改设置"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Change avatar"
 msgstr "更改头像"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Log out"
 msgstr "退出"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned until %(ban_expires)s"
 msgstr "%(username)s 已被封禁直到 %(ban_expires)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is banned"
 msgstr "%(username)s 已被封禁"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is hiding presence"
 msgstr "%(username)s 在隐身"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online (hidden)"
 msgstr "%(username)s 在线(隐身)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s (hidden)"
 msgstr "%(username)s 最后一次出现于 %(last_click)s(隐身)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s is online"
 msgstr "%(username)s 在线"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "%(username)s was last seen %(last_click)s"
 msgstr "%(username)s 最后一次出现于 %(last_click)s"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Banned"
 msgstr "被封禁"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online (hidden)"
 msgstr "在线(隐身)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline (hidden)"
 msgstr "离线(隐身)"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Online"
 msgstr "在线"
 
-#: static/misago/js/misago.js:20
+#: static/misago/js/misago.js:21
 msgid "Offline"
 msgstr "离线"
 
@@ -2424,19 +2494,19 @@ msgstr[0] "%(followers)s的关注者"
 msgid "No users have posted any new messages during last %(days)s days."
 msgstr "已经%(days)s天没有用户发布消息。"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Rank"
 msgstr "级别"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:21 static/misago/js/misago.js:22
 msgid "Ranked posts"
 msgstr "有级别的帖子"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "Total posts"
 msgstr "帖子总数"
 
-#: static/misago/js/misago.js:21
+#: static/misago/js/misago.js:22
 msgid "%(posters)s most active poster from last %(days)s days."
 msgid_plural "%(posters)s most active posters from last %(days)s days."
 msgstr[0] "%最近%(days)s天最活跃的发布者%(poster)s"
@@ -2466,25 +2536,25 @@ msgstr "是"
 msgid "no"
 msgstr "没有"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid ""
 "Private threads are threads which only those that started them and those "
 "they have invited may see and participate in."
 msgstr "私人主题帖只有那些启动他们和那些他们已经邀请可以看到并参与。"
 
-#: static/misago/js/misago.js:22
+#: static/misago/js/misago.js:23
 msgid "You aren't participating in any private threads."
 msgstr "您没有参与任何私人主题帖。"
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Lost connection with application."
 msgstr "与应用程序的连接失败"
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Action link is invalid."
 msgstr "操作链接无效。"
 
-#: static/misago/js/misago.js:23 static/misago/js/misago.js:24
+#: static/misago/js/misago.js:24
 msgid "Unknown error has occured."
 msgstr "发生未知错误。"
 
@@ -2520,15 +2590,23 @@ msgstr "您已经在编辑调查。确定要放弃吗?"
 msgid "You don't have permission to perform this action."
 msgstr "您没有执行此操作的权限。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "You are banned"
 msgstr "您被封禁了"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "This field is required."
 msgstr "这是必填栏。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
+msgid "You have to accept the terms of service."
+msgstr ""
+
+#: static/misago/js/misago.js:25
+msgid "You have to accept the privacy policy."
+msgstr ""
+
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at least %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2537,7 +2615,7 @@ msgid_plural ""
 "%(show_value)s)."
 msgstr[0] "确保此值至少有%(limit_value)s个字符(现有%(show_value)s)。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid ""
 "Ensure this value has at most %(limit_value)s character (it has "
 "%(show_value)s)."
@@ -2546,21 +2624,21 @@ msgid_plural ""
 "%(show_value)s)."
 msgstr[0] "确保此值至多为%(limit_value)s字符(现有%(show_value)s)。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username must be at least %(limit_value)s character long."
 msgid_plural "Username must be at least %(limit_value)s characters long."
 msgstr[0] "用户名必须至少%(limit_value)s字符长"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username cannot be longer than %(limit_value)s character."
 msgid_plural "Username cannot be longer than %(limit_value)s characters."
 msgstr[0] "用户名不可以超过%(limit_value)s字符。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Username can only contain latin alphabet letters and digits."
 msgstr "用户名只可以包含拉丁字母和数字。"
 
-#: static/misago/js/misago.js:24
+#: static/misago/js/misago.js:25
 msgid "Valid password must be at least %(limit_value)s character long."
 msgid_plural ""
 "Valid password must be at least %(limit_value)s characters long."

+ 9 - 13
misago/markup/api.py

@@ -1,31 +1,27 @@
+from rest_framework import status
 from rest_framework.decorators import api_view
 from rest_framework.response import Response
-from rest_framework import serializers
-
-from misago.threads.validators import validate_post_length
 
 from . import common_flavour, finalise_markup
+from .serializers import MarkupSerializer
 
 
 @api_view(['POST'])
 def parse_markup(request):
     serializer = MarkupSerializer(data=request.data)
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        errors_list = list(serializer.errors.values())[0]
+        return Response(
+            {'detail': errors_list[0]},
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
     parsing_result = common_flavour(
         request,
         request.user,
-        serializer.data['markup'],
+        serializer.data['post'],
         force_shva=True,
     )
     finalised = finalise_markup(parsing_result['parsed_text'])
 
     return Response({'parsed': finalised})
-
-
-class MarkupSerializer(serializers.Serializer):
-    markup = serializers.CharField(allow_blank=True)
-
-    def validate_markup(self, data):
-        validate_post_length(data)
-        return data

+ 9 - 0
misago/markup/context_processors.py

@@ -0,0 +1,9 @@
+from django.urls import reverse
+
+
+def preload_api_url(request):
+    request.frontend_context.update({
+        'PARSE_MARKUP_API': reverse('misago:api:parse-markup'),
+    })
+
+    return {}

+ 11 - 0
misago/markup/serializers.py

@@ -0,0 +1,11 @@
+from rest_framework import serializers
+
+from misago.threads.validators import validate_post_length
+
+
+class MarkupSerializer(serializers.Serializer):
+    post = serializers.CharField(required=False, allow_blank=True)
+
+    def validate(self, data):
+        validate_post_length(data.get('post', ''))
+        return data

+ 26 - 74
misago/markup/tests/test_api.py

@@ -17,94 +17,46 @@ class ParseMarkupApiTests(AuthenticatedUserTestCase):
         self.logout_user()
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This action is not available to guests.",
-            },
-        )
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_no_data(self):
         """api handles no data"""
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'markup': ["This field is required."],
-            },
-        )
+        self.assertContains(response, "You have to enter a message.", status_code=400)
 
     def test_invalid_data(self):
         """api handles post that is invalid type"""
         response = self.client.post(self.api_link, '[]', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'non_field_errors': ["Invalid data. Expected a dictionary, but got list."],
-            },
-        )
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         response = self.client.post(self.api_link, '123', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'non_field_errors': ["Invalid data. Expected a dictionary, but got int."],
-            },
-        )
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         response = self.client.post(self.api_link, '"string"', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'non_field_errors': ["Invalid data. Expected a dictionary, but got str."],
-            },
-        )
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         response = self.client.post(self.api_link, 'malformed', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'detail': 'JSON parse error - Expecting value: line 1 column 1 (char 0)',
-            },
-        )
+        self.assertContains(response, "JSON parse error", status_code=400)
 
-    def test_empty_markup(self):
-        """api handles empty markup"""
-        response = self.client.post(self.api_link, {'markup': ''})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'markup': ["You have to enter a message."],
-            },
-        )
+    def test_empty_post(self):
+        """api handles empty post"""
+        response = self.client.post(self.api_link, {'post': ''})
+        self.assertContains(response, "You have to enter a message.", status_code=400)
 
         # regression test for #929
-        response = self.client.post(self.api_link, {'markup': '\n'})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'markup': ["You have to enter a message."],
-            },
-        )
-
-    def test_invalid_markup(self):
-        """api handles invalid markup type"""
-        response = self.client.post(self.api_link, {'markup': 123})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'markup': [
-                    "Posted message should be at least 5 characters long (it has 3)."
-                ],
-            },
-        )
-        
-    def test_valid_markup(self):
-        """api returns parsed markup for valid markup"""
-        response = self.client.post(self.api_link, {'markup': 'Lorem ipsum dolor met!'})
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(
-            response.json(), {
-                'parsed': '<p>Lorem ipsum dolor met!</p>',
-            },
-        )
+        response = self.client.post(self.api_link, {'post': '\n'})
+        self.assertContains(response, "You have to enter a message.", status_code=400)
+
+    def test_invalid_post(self):
+        """api handles invalid post type"""
+        response = self.client.post(self.api_link, {'post': 123})
+        self.assertContains(
+            response,
+            "Posted message should be at least 5 characters long (it has 3).",
+            status_code=400
+        )
+
+    def test_valid_post(self):
+        """api returns parsed markup for valid post"""
+        response = self.client.post(self.api_link, {'post': 'Lorem ipsum dolor met!'})
+        self.assertContains(response, "<p>Lorem ipsum dolor met!</p>")

+ 4 - 0
misago/project_template/cron.txt

@@ -9,4 +9,8 @@
 25 0 * * * python manage.py clearsessions
 25 0 * * * python manage.py clearsocial
 25 0 * * * python manage.py invalidatebans
+0 2 * * * python manage.py removeoldips
 0 1 * * * python manage.py deletemarkedusers
+25 1 * * * python manage.py deleteinactiveusers
+0 2 * * * python manage.py expireuserdatadownloads
+0 7 * * * python manage.py prepareuserdatadownloads

+ 53 - 20
misago/project_template/project_name/settings.py

@@ -138,22 +138,19 @@ STATICFILES_DIRS = [
 ]
 
 
-# Path to default logo image used in navbar, should be relative to STATIC_URL.
-
-MISAGO_LOGO = 'logo.png'
-
-
 # Email configuration
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-backend
 
 EMAIL_HOST = 'localhost'
 EMAIL_PORT = 25
 
+
 # If either of these settings is empty, Django won't attempt authentication.
 
 EMAIL_HOST_USER = ''
 EMAIL_HOST_PASSWORD = ''
 
+
 # Default email address to use for various automated correspondence from the site manager(s).
 
 DEFAULT_FROM_EMAIL = 'Forums <%s>' % EMAIL_HOST_USER
@@ -192,9 +189,8 @@ INSTALLED_APPS = [
     'social_django',
 
     # Misago apps
-    'misago.acl',
     'misago.admin',
-    'misago.api',
+    'misago.acl',
     'misago.core',
     'misago.conf',
     'misago.markup',
@@ -220,7 +216,7 @@ MIDDLEWARE = [
     'debug_toolbar.middleware.DebugToolbarMiddleware',
 
     'misago.users.middleware.RealIPMiddleware',
-    'misago.api.middleware.FrontendContextMiddleware',
+    'misago.core.middleware.frontendcontext.FrontendContextMiddleware',
 
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -300,13 +296,15 @@ TEMPLATES = [
                 'misago.legal.context_processors.legal_links',
 
                 # Data preloaders
-                'misago.categories.context_processors.preload_categories_json',
                 'misago.conf.context_processors.preload_settings_json',
+                'misago.core.context_processors.current_link',
+                'misago.markup.context_processors.preload_api_url',
+                'misago.threads.context_processors.preload_threads_urls',
                 'misago.users.context_processors.preload_user_json',
 
                 # Note: keep frontend_context processor last for previous processors
                 # to be able to expose data UI app via request.frontend_context
-                'misago.api.context_processors.frontend_context',
+                'misago.core.context_processors.frontend_context',
             ],
         },
     },
@@ -347,12 +345,12 @@ DEBUG_TOOLBAR_PANELS = [
 
 REST_FRAMEWORK = {
     'DEFAULT_PERMISSION_CLASSES': [
-        'misago.api.rest_permissions.IsAuthenticatedOrReadOnly',
+        'misago.core.rest_permissions.IsAuthenticatedOrReadOnly',
     ],
     'DEFAULT_RENDERER_CLASSES': [
         'rest_framework.renderers.JSONRenderer',
     ],
-    'EXCEPTION_HANDLER': 'misago.api.exceptionhandler.handle_api_exception',
+    'EXCEPTION_HANDLER': 'misago.core.exceptionhandler.handle_api_exception',
     'UNAUTHENTICATED_USER': 'misago.users.models.AnonymousUser',
     'URL_FORMAT_OVERRIDE': None,
 }
@@ -361,8 +359,16 @@ REST_FRAMEWORK = {
 # Misago specific settings
 # https://misago.readthedocs.io/en/latest/developers/settings.html
 
+# Complete HTTP address to your Misago site homepage. Misago relies on this address to create
+# links in e-mails that are sent to site users.
+# On Misago admin panel home page you will find a message telling you if you have entered the
+# correct value, or what value is correct in case you've didn't.
+
+MISAGO_ADDRESS = 'http://my-misago-site.com/'
+
+
 # PostgreSQL text search configuration to use in searches
-# Defaults to "simple", for list of installed configurations run "\dF" in "psql"
+# Defaults to "simple", for list of installed configurations run "\dF" in "psql".
 # Standard configs as of PostgreSQL 9.5 are: dutch, english, finnish, french,
 # german, hungarian, italian, norwegian, portuguese, romanian, russian, simple,
 # spanish, swedish and turkish
@@ -371,12 +377,46 @@ REST_FRAMEWORK = {
 MISAGO_SEARCH_CONFIG = 'simple'
 
 
+# Allow users to download their personal data
+# Enables users to learn what data about them is being held by the site without having to contact
+# site's administrators.
+
+MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
+
+# Path to the directory that Misago should use to prepare user data downloads.
+# Should not be accessible from internet.
+
+MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = os.path.join(BASE_DIR, 'userdata')
+
+
+# Allow users to delete their accounts
+# Lets users delete their own account on the site without having to contact site administrators.
+# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
+# data about user left behind after user is deleted.
+
+MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True
+
+
+# Automatically delete new user accounts that weren't activated in specified time
+# If you rely on admin review of new registrations, make this period long, disable
+# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
+# keep it short to give users a chance to retry on their own after few days pass.
+
+MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 2
+
+
 # Path to directory containing avatar galleries
 # Those galleries can be loaded by running loadavatargallery command
 
 MISAGO_AVATAR_GALLERY = os.path.join(BASE_DIR, 'avatargallery')
 
 
+# Specifies the number of days that IP addresses are stored in the database before removing.
+# Change this setting to None to never remove old IP addresses.
+
+MISAGO_IP_STORE_TIME = 50
+
+
 # Profile fields
 
 MISAGO_PROFILE_FIELDS = [
@@ -401,13 +441,6 @@ MISAGO_PROFILE_FIELDS = [
         'name': _("IP address"),
         'fields': [
             'misago.users.profilefields.default.JoinIpField',
-            'misago.users.profilefields.default.LastIpField',
         ],
     },
 ]
-
-
-# Allow users to delete their own accounts?
-# Providing such feature is required by EU law from entities that process europeans personal data.
-
-MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True

+ 1 - 1
misago/project_template/project_name/urls.py

@@ -22,7 +22,7 @@ from django.views.decorators.cache import cache_page
 from django.views.decorators.http import last_modified
 from django.views.i18n import JavaScriptCatalog
 
-from misago.admin.forms import AdminAuthenticationForm
+from misago.users.forms.auth import AdminAuthenticationForm
 
 
 admin.autodiscover()

+ 3 - 0
misago/project_template/userdata/README.txt

@@ -0,0 +1,3 @@
+This directory is used by Misago to prepare user data downloads.
+
+Make sure it's not accessible from internet.

+ 2 - 2
misago/readtracker/signals.py

@@ -1,6 +1,6 @@
 from django.dispatch import Signal, receiver
 
-from misago.categories.models import PRIVATE_THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 from misago.categories.signals import delete_category_content, move_category_content
 from misago.threads.signals import merge_thread, move_thread, merge_post, move_post
 
@@ -50,7 +50,7 @@ def decrease_unread_private_count(sender, **kwargs):
     user = sender
     thread = kwargs['thread']
 
-    if thread.category.thread_type.root_name != PRIVATE_THREADS_ROOT:
+    if thread.category.thread_type.root_name != PRIVATE_THREADS_ROOT_NAME:
         return
 
     if user.unread_private_threads:

+ 6 - 2
misago/search/context_processors.py

@@ -18,13 +18,17 @@ def search_providers(request):
         # with non-misago's anonymous user model that has no acl support
         return {}
 
-    request.frontend_context['search'] = []
+    request.frontend_context['SEARCH_URL'] = reverse('misago:search')
+    request.frontend_context['SEARCH_API'] = reverse('misago:api:search')
+    request.frontend_context['SEARCH_PROVIDERS'] = []
 
     for provider in allowed_providers:
-        request.frontend_context['search'].append({
+        request.frontend_context['SEARCH_PROVIDERS'].append({
             'id': provider.url,
             'name': six.text_type(provider.name),
             'icon': provider.icon,
+            'url': reverse('misago:search', kwargs={'search_provider': provider.url}),
+            'api': reverse('misago:api:search', kwargs={'search_provider': provider.url}),
             'results': None,
             'time': None,
         })

+ 1 - 4
misago/search/tests/test_api.py

@@ -18,10 +18,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.test_link)
 
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to search site."
-        })
+        self.assertContains(response, "have permission to search site", status_code=403)
 
     def test_no_phrase(self):
         """api handles no search query"""

+ 3 - 0
misago/search/views.py

@@ -29,5 +29,8 @@ def search(request, search_provider):
     else:
         raise Http404()
 
+    if 'q' in request.GET:
+        request.frontend_context['SEARCH_QUERY'] = request.GET.get('q')
+
     return render(request, 'misago/search.html')
 

+ 1 - 1
misago/static/misago/css/misago.css

@@ -1 +1 @@
-/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.btn,.caret,img{vertical-align:middle}hr,img{border:0}body,figure{margin:0}.img-thumbnail,.table,label{max-width:100%}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}.alerts-snackbar,.form-control-feedback,a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}.img-thumbnail,body{background-color:#fff}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#212121}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#651fff;text-decoration:none}a:focus,a:hover{color:#4100d2;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}dl,ol,ul{margin-top:0}.lead,address,dl{margin-bottom:20px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{font-size:16px;font-weight:300;line-height:1.4}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#651fff}a.text-primary:focus,a.text-primary:hover{color:#4900eb}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#651fff}a.bg-primary:focus,a.bg-primary:hover{background-color:#4900eb}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:700px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{font-style:normal}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.badge,.btn,.dropdown-header,.input-group-btn,.label,.material-icon{white-space:nowrap}.pre-scrollable{overflow-y:scroll}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#616161}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.btn-block,input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;touch-action:manipulation;cursor:pointer;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#757575;text-decoration:none}.btn.active,.btn:active{outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{background-color:#f5f5f5}.btn-default.focus,.btn-default:focus{color:#757575;background-color:#dcdcdc;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#757575;background-color:#dcdcdc;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#757575;background-color:#cacaca;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#f5f5f5;border-color:#ccc}.btn-default .badge{color:#f5f5f5;background-color:#757575}.btn-primary{background-color:#a36eff}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#823bff;border-color:#4d00d4}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#823bff;border-color:#6b18ff}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#6b18ff;border-color:#4d00d4}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#a36eff;border-color:#9255ff}.btn-primary .badge{color:#a36eff;background-color:#fff}.btn-success{background-color:#00c853}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#00953e;border-color:#002f14}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#00953e;border-color:#00712f}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#00712f;border-color:#002f14}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#00c853;border-color:#00af48}.btn-success .badge{color:#00c853;background-color:#fff}.btn-info{color:#fff;background-color:#3d5afe;border-color:#2444fe}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#0a2ffe;border-color:#0119a1}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#0a2ffe;border-color:#0123e3}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#0123e3;border-color:#0119a1}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#3d5afe;border-color:#2444fe}.btn-info .badge{color:#3d5afe;background-color:#fff}.btn-warning{color:#fff;background-color:#ef6c00;border-color:#d66000}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#bc5500;border-color:#562700}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#bc5500;border-color:#984500}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#984500;border-color:#562700}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#ef6c00;border-color:#d66000}.btn-warning .badge{color:#ef6c00;background-color:#fff}.btn-danger{background-color:#ef5350}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#eb2521;border-color:#98110e}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#eb2521;border-color:#d51713}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#d51713;border-color:#98110e}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#ef5350;border-color:#ed3c39}.btn-danger .badge{color:#ef5350;background-color:#fff}.btn-link{color:#651fff;font-weight:400;border-radius:0}.alert .alert-link,.label{font-weight:700}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#4100d2;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block}.btn-block+.btn-block{margin-top:5px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.nav>li,.nav>li>a,.open>.dropdown-menu{display:block}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#424242}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#651fff}.dropdown-header,.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.nav>li.disabled>a{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>a{outline:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.nav>li,.nav>li>a,.navbar{position:relative}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:700px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#651fff}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#651fff}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{min-height:54px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:700px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:17px 15px;font-size:18px;line-height:20px;height:54px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:700px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:10px;margin-bottom:10px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:8.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:699px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:700px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:17px;padding-bottom:17px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:10px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:699px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:700px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-text{float:left;margin-left:15px;margin-right:15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-sm{margin-top:12px;margin-bottom:12px}.navbar-btn.btn-xs{margin-top:16px;margin-bottom:16px}.navbar-text{margin-top:17px;margin-bottom:17px}.alert,.breadcrumb{margin-bottom:20px}@media (min-width:700px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.progress-bar{float:left}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:699px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#e0e0e0}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#e7e7e7}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#fff}@media (max-width:699px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#e0e0e0}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#e7e7e7}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#e0e0e0}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#e0e0e0}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#ccc}.breadcrumb{padding:8px 15px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#43a047;border-color:#43a047;color:#fff}.alert-success hr{border-top-color:#3b8e3f}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#448aff;border-color:#448aff;color:#fff}.alert-info hr{border-top-color:#2a7aff}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#ef6c00;border-color:#ef6c00;color:#fff}.alert-warning hr{border-top-color:#d66000}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#f44336;border-color:#f44336;color:#fff}.alert-danger hr{border-top-color:#f32c1e}.alert-danger .alert-link{color:#e6e6e6}.label{display:inline;padding:.2em .6em .3em;font-size:75%;line-height:1;color:#fff;text-align:center;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#651fff}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#4900eb}.label-success{background-color:#43a047}.label-success[href]:focus,.label-success[href]:hover{background-color:#347c37}.label-info{background-color:#3d5afe}.label-info[href]:focus,.label-info[href]:hover{background-color:#0a2ffe}.label-warning{background-color:#ef6c00}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#bc5500}.label-danger{background-color:#f44336}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#ea1c0d}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{margin-bottom:20px;background-color:#eee;border-radius:3px}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.progress-bar{width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#651fff;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#00c853}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#3d5afe}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#ff9100}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#ff1744}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.badge,.material-icon{vertical-align:middle}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #eee}.list-group-item:first-child,.panel-heading{border-top-right-radius:3px;border-top-left-radius:3px}.list-group-item:last-child,.panel-footer{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.list-group-item:last-child{margin-bottom:0}a.list-group-item,button.list-group-item{color:#777}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#212121;background-color:#fff}button.list-group-item{width:100%;text-align:left}.badge,.pager{text-align:center}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#651fff;background-color:#fff;border-color:#eee}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#fff}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel,.panel-footer{background-color:#fff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:14px 15px;border-bottom:1px solid transparent}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:14px 15px;border-top:1px solid #ddd}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#fff;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#fff;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#651fff}.panel-primary>.panel-heading{color:#fff;background-color:#651fff;border-color:#651fff}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#651fff}.panel-primary>.panel-heading .badge{color:#651fff;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#651fff}.panel-success{border-color:#00c853}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#00c853}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#00c853}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#00c853}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#f44336}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#f44336}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f44336}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f44336}.pager{padding-left:0;margin:20px 0;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#651fff;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.close,.list-group-item>.badge{float:right}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#4100d2;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#651fff;border-color:#651fff;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.close{font-weight:700;line-height:1}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fff;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{font-size:21px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;background-color:#777;border-radius:10px}.badge:empty,.modal{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#651fff;background-color:#fff}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.affix,.auth-message{position:fixed}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.header-stats,.page-header .breadcrumb,.page-header .go-back-sm,.page-header h1{text-shadow:0 1px 1px #6200ea}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}@media screen and (min-width:992px){.md-margin-top-no{margin-top:0!important}}@media screen and (min-width:768px) and (max-width:991px){.sm-margin-top{margin-top:20px!important}.sm-margin-top-no{margin-top:0!important}.sm-margin-top-half{margin-top:6.6px!important}.sm-align-row-buttons{margin-top:5px!important}}@media screen and (max-width:767px){.xs-margin-top{margin-top:20px!important}.xs-margin-top-half{margin-top:6.6px!important}}.auth-message{background-color:#eee;width:100%;top:-100%;left:0;z-index:1070;transition:top .3s ease}.auth-message.show{top:0;bottom:auto}.auth-message p{padding:5px 0}@media screen and (max-width:991px){body,html{overflow-x:hidden}.auth-message{text-align:center}.auth-message .btn{padding:10px 16px;font-size:18px}}.alerts-snackbar{position:fixed;top:-100%;width:100%;z-index:1060;text-align:center;font-size:18px;transition:top .3s ease}.alerts-snackbar.in{top:0;transition:top .2s ease}.alerts-snackbar p{display:inline-block;border-radius:0 0 4px 4px;margin:0;pointer-events:all}.loader{width:100%;height:49px;text-align:center}.loader.loader-spaced{margin:40px 0}.loader-spinning-wheel{width:49px;height:49px;margin:0 auto;border:3px solid #777;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:cssload-spin 575ms infinite linear;-o-animation:cssload-spin 575ms infinite linear;-ms-animation:cssload-spin 575ms infinite linear;-webkit-animation:cssload-spin 575ms infinite linear;-moz-animation:cssload-spin 575ms infinite linear}@keyframes cssload-spin{100%{transform:rotate(360deg)}}@-o-keyframes cssload-spin{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes cssload-spin{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes cssload-spin{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes cssload-spin{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}.navbar{margin-bottom:0}.navbar .navbar-full .navbar-brand{padding-top:11px;padding-bottom:11px;font-size:21px}.navbar .navbar-full .navbar-brand>*{display:inline-block;vertical-align:middle}.navbar .navbar-full .navbar-brand img{height:32px;margin-right:6px}.navbar .navbar-full .navbar-nav>li>a{font-size:16.8px}.navbar .navbar-full .navbar-icon{display:block;height:54px;padding:13px 15px;position:relative;color:#e0e0e0}.navbar .navbar-full .navbar-icon:focus,.navbar .navbar-full .navbar-icon:hover{color:#fff;background-color:transparent}.navbar .navbar-full .navbar-icon .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.navbar .navbar-full .navbar-icon .badge{background-color:#f44336;position:absolute;top:6px;right:6px;font-size:9px}.nav-side>a>.material-icon,.navbar ul.navbar-compact-nav>li>a>.material-icon{font-size:24px;line-height:24px}.navbar .navbar-full .open .navbar-icon,.navbar .navbar-full .open .navbar-icon:focus,.navbar .navbar-full .open .navbar-icon:hover{background-color:#e7e7e7;color:#555}.navbar .navbar-full .nav-guest,.navbar .navbar-full .nav-user{float:right}.navbar .navbar-full .nav-guest .navbar-btn,.navbar .navbar-full .nav-user .navbar-btn{margin-left:15px}.navbar .navbar-full .nav-user .dropdown-toggle{padding:10px}.navbar .navbar-full .nav-user .dropdown-toggle img{width:34px;height:34px}.navbar ul.navbar-compact-nav{border-collapse:collapse;display:table;margin:0;table-layout:fixed;width:100%}.navbar ul.navbar-compact-nav>li{display:table-cell;width:100%}.navbar ul.navbar-compact-nav>li>a,.navbar ul.navbar-compact-nav>li>button{background:0 0;border:none;display:block;padding-top:13px;padding-bottom:13px;width:100%;color:#e0e0e0;text-align:center}.navbar ul.navbar-compact-nav>li>a.active,.navbar ul.navbar-compact-nav>li>a:focus,.navbar ul.navbar-compact-nav>li>a:hover,.navbar ul.navbar-compact-nav>li>button.active,.navbar ul.navbar-compact-nav>li>button:focus,.navbar ul.navbar-compact-nav>li>button:hover{color:#212121;background-color:#fafafa}.navbar ul.navbar-compact-nav>li>a>img,.navbar ul.navbar-compact-nav>li>button>img{width:24px;height:24px}.navbar ul.navbar-compact-nav>li>button{display:inline-block}@media (max-width:700px){.navbar.navbar-misago{min-height:auto}}.modal-body .form-group,.toolbar{min-height:34px}.navbar-misago .navbar-desktop-nav{display:none}@media (min-width:700px){.navbar-misago ul.navbar-compact-nav{display:none}.navbar-misago .navbar-desktop-nav{display:block}}.nav-side>a>.material-icon{margin:-5px 10px -5px -5px;position:relative;bottom:1px;width:24px;height:24px}.nav-side>a .badge{position:relative;top:1px}@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(../fonts/MaterialIcons-Regular.eot);src:local('Material Icons'),local('MaterialIcons-Regular'),url(../fonts/MaterialIcons-Regular.woff2) format('woff2'),url(../fonts/MaterialIcons-Regular.woff) format('woff'),url(../fonts/MaterialIcons-Regular.ttf) format('truetype')}.material-icon{font-family:'Material Icons';font-weight:400;font-style:normal;display:inline-block;width:1em;height:1em;line-height:1;text-align:center;text-transform:none;letter-spacing:normal;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:'liga'}.misago-markup h1,.misago-markup h2,.misago-markup h3,.misago-markup h4,.misago-markup h5,.misago-markup h6,.misago-markup p,.page-header h1,.post-attachments .attachment-name,.post-feed .post-heading .btn{word-wrap:break-word}.modal-toolbar{background:#eee;border-bottom:1px solid #e5e5e5;overflow:auto;padding:6px 12px}.modal-toolbar .pull-left{margin-right:8px}.modal-toolbar p{padding:5px 0;margin-bottom:0}@media screen and (max-width:991px){.modal-message{text-align:center}.modal-message .message-icon{margin:30px}.modal-message .message-icon .material-icon{font-size:160px}}@media screen and (min-width:992px){.modal-message .modal-body{padding-top:20px;padding-bottom:30px}.modal-message .message-icon{float:left;position:relative;left:7px}.modal-message .message-icon .material-icon{font-size:50px}.modal-message .message-body{margin-left:75px;margin-top:10px}.modal-message .message-body p{margin-top:20px}.modal-message .message-body .lead{margin-top:0;margin-bottom:0}}.modal-loader{padding:50px 0}.modal-loader .loader{width:100%;height:80px;text-align:center}.modal-loader .loader-spinning-wheel{width:80px;height:80px}.list-item-errors{margin-bottom:20px}.list-errored-items li:last-child .list-item-errors{margin-bottom:0}.modal-post-likers .media-list{margin:0}.modal-post-likers .item-title{display:block}.has-feedback .material-icon.form-control-feedback{top:6px;right:24px;font-size:1.42857143;line-height:1.42857143}.well.well-form.well-done{font-size:18px;text-align:center}.well.well-form.well-done .message-icon{margin-bottom:10px;font-size:90px;line-height:90px}.well.well-form.well-done .message-body{margin-bottom:20px}.well.well-form.well-noscript{font-size:18px;text-align:center}.well.well-form.well-noscript .message-icon{margin-bottom:10px;font-size:90px;line-height:90px}.btn.btn-select,.btn.btn-yes-no{background:0 0;border:1px solid #eee}.btn.btn-select .material-icon,.btn.btn-yes-no .material-icon{margin:-4px 8px -4px 0;position:relative;bottom:1px;width:20px;height:20px;font-size:20px;line-height:20px}@media screen and (max-width:767px){.btn.btn-yes-no{width:100%;overflow:auto}.btn.btn-yes-no .material-icon{float:left;margin-top:1px}.btn.btn-yes-no .btn-text{display:block;margin-left:30px;text-align:left;white-space:normal}}input.hidden-file-upload{position:absolute;top:-9999px;left:-9999px}.form-search{position:relative}.form-search .form-control{padding-right:30px}.form-search .material-icon{position:absolute;top:5px;right:5px;color:#777;font-size:24px;line-height:24px;pointer-events:none}.btn.btn-loading,.btn.btn-loading:active,.btn.btn-loading:focus,.btn.btn-loading:hover,.btn.btn-loading:link,.btn.btn-loading:visited{color:transparent}.btn.btn-loading .loader,.btn.btn-loading:active .loader,.btn.btn-loading:focus .loader,.btn.btn-loading:hover .loader,.btn.btn-loading:link .loader,.btn.btn-loading:visited .loader{height:20px;margin-top:-20px}.btn.btn-loading .loader>div,.btn.btn-loading:active .loader>div,.btn.btn-loading:focus .loader>div,.btn.btn-loading:hover .loader>div,.btn.btn-loading:link .loader>div,.btn.btn-loading:visited .loader>div{width:20px;height:20px}.btn.btn-loading.btn-default .loader>div{border-top-color:#757575;border-bottom-color:#757575}.btn.btn-loading.btn-danger .loader>div,.btn.btn-loading.btn-info .loader>div,.btn.btn-loading.btn-primary .loader>div,.btn.btn-loading.btn-success .loader>div,.btn.btn-loading.btn-warning .loader>div{border-top-color:#fff;border-bottom-color:#fff}.btn .material-icon{margin-right:3px;position:relative;bottom:1px}.btn-icon .material-icon{margin:-1px -4px;width:20px;height:20px;font-size:20px;line-height:20px}.btn-icon .btn-text{margin-left:10px}.btn-icon .btn-text-left{margin-right:10px}.btn-block.btn-icon{padding-left:0;padding-right:0}.dropdown-menu>li>.btn-link,.dropdown-menu>li>a,.modal-menu>li>.btn-link,.modal-menu>li>a{display:block;border:none;clear:both;float:none;padding:6px 20px;width:100%;color:#333;font-weight:400;line-height:1.42857143;text-align:left;white-space:nowrap}.pager-more,.user-dropdown .guest-preview,li.dropdown-search-message{text-align:center}.dropdown-menu>li>.btn-link:active,.dropdown-menu>li>.btn-link:focus,.dropdown-menu>li>.btn-link:hover,.dropdown-menu>li>a:active,.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover,.modal-menu>li>.btn-link:active,.modal-menu>li>.btn-link:focus,.modal-menu>li>.btn-link:hover,.modal-menu>li>a:active,.modal-menu>li>a:focus,.modal-menu>li>a:hover{background-color:#424242;color:#FAFAFA;text-decoration:none}.dropdown-menu>li>.btn-link .material-icon,.dropdown-menu>li>a .material-icon,.modal-menu>li>.btn-link .material-icon,.modal-menu>li>a .material-icon{margin:-2px 7px -2px 0;position:relative;bottom:1px;font-size:18px}.dropdown-menu>li>.btn-link .badge,.dropdown-menu>li>a .badge,.modal-menu>li>.btn-link .badge,.modal-menu>li>a .badge{float:right;position:relative;top:1px}.modal-menu{margin:20px 0;padding:0}.modal-menu>li{margin:6.67px 0;padding:0;list-style:none}.dropdown-menu{width:210px}.dropdown-menu .dropdown-footer{padding:6px 20px}.dropdown-menu .dropdown-buttons{padding:2px 20px 7px}.dropdown-menu .dropdown-buttons .btn{margin:4px 0}.mobile-dropdown{position:relative}.compact-nav.open>.dropdown-menu,.mobile-dropdown.open>.dropdown-menu{border:none;border-radius:0;display:block;margin:0;width:100%}.user-dropdown .guest-preview .row{margin:0}.navbar .user-dropdown{width:240px}.user-dropdown .dropdown-header{padding:6px 20px;font-size:18px}@media screen and (min-width:992px){.category-picker .dropdown-menu{width:300px}}.category-picker .dropdown-menu>li>.btn-link{white-space:normal;word-wrap:break-word}.dropdown-search-thread h5,.editor-attachment-complete .editor-attachment-details abbr,.page-tabs ul,.page-tabs ul a,.page-tabs ul li,.post-attachments abbr{white-space:nowrap}@media screen and (max-width:767px){.dropdown-menu.stick-to-bottom{border-radius:0;border:none;max-height:300px;overflow-y:auto;-webkit-box-shadow:0 0 30px #777;box-shadow:0 0 30px #777;clear:both;top:auto;width:100%;position:fixed;bottom:0;margin:0;padding:0 0 20px}.dropdown-menu.stick-to-bottom li{float:none;margin:0;clear:both}.dropdown-menu.stick-to-bottom li>.btn,.dropdown-menu.stick-to-bottom li>a{padding-top:15px;padding-bottom:15px;border-bottom:1px solid #e5e5e5}}.navbar-misago .dropdown-menu.dropdown-search-results{margin:0;padding-top:0;width:400px;left:auto;right:0}.dropdown-search-results .form-group{margin:0;padding:12px}.dropdown-search-thread{width:100%}.dropdown-search-thread h5{margin:0;overflow:hidden;text-overflow:ellipsis}.dropdown-search-thread small{display:block;margin:4px 0 0}.page-header-bg{margin-bottom:20px}.page-header{margin:0;padding:40px 0}.page-header h1{margin:0}.page-header .btn-aligned{float:right;margin-left:8px}.page-header .btn-aligned.pull-left{margin-left:0;margin-right:8px}@media screen and (min-width:992px){.page-header .container>.row h1{margin-top:-3px}.page-header .container>.row .row{margin-top:5px}}@media screen and (max-width:991px){.container h1{font-size:22.5px}.container .btn-icon .material-icon{width:24px;height:24px;margin:-3px 0;font-size:24px;line-height:24px}}.go-back-sm .material-icon,.header-stats .list-inline li .status-icon{width:18px;height:18px;font-size:18px;line-height:18px;position:relative}.page-breadcrumbs{margin-bottom:20px}.page-breadcrumbs .breadcrumb{background:0 0;margin:-20px 0 0;padding:0;overflow:auto}.page-breadcrumbs .breadcrumb li,.page-breadcrumbs .breadcrumb li:before{display:block;float:left}.page-breadcrumbs .breadcrumb li:before{margin-left:4px}.go-back-sm{margin-top:-20px;margin-bottom:20px}.go-back-sm .material-icon{top:-1px}.page-breadcrumbs .go-back-sm{margin-bottom:0}.header-stats{margin-top:20px;margin-bottom:-20px}.header-stats .list-inline{margin-bottom:0}.header-stats .list-inline li{margin-bottom:0;margin-right:8px;overflow:auto;vertical-align:top}.header-stats .list-inline li .status-icon{margin-right:4px;bottom:1px}.header-stats .list-inline li>.icon-legend,.header-stats .list-inline li>.material-icon{float:left}.header-stats .list-inline li>.material-icon{margin-right:4px;position:relative;top:3px}.page-header .page-tabs{margin-bottom:-40px}.page-header .page-tabs .nav>li{margin:0}.page-header .page-tabs .nav>li>a{border-radius:0}.page-tabs ul{display:block;overflow-x:auto}.page-tabs ul a,.page-tabs ul li{display:inline-block;float:none}.page-tabs ul a .material-icon,.page-tabs ul li .material-icon{margin-right:6px}@media screen and (max-width:767px){.page-tabs .container{padding-left:0;padding-right:0}.page-tabs li a{padding-left:26.6px;padding-right:26.6px}.header-stats+.page-tabs{margin-top:30px}}.title-edit-form{margin-bottom:12px}@media screen and (max-width:991px){.header-stats .list-inline{font-size:12px}.header-stats .list-inline li>.material-icon{top:2px}.panel-message-body{text-align:center}.panel-message-body .message-icon{margin:30px}.panel-message-body .message-icon .material-icon{font-size:160px}}@media screen and (min-width:992px){.panel-message-body{padding:20px 20px 30px}.panel-message-body .message-icon{float:left}.panel-message-body .message-icon .material-icon{font-size:50px}.panel-message-body .message-body{margin-left:65px;margin-top:10px}.panel-message-body .message-body .lead{margin-bottom:0}.panel-message-body .message-body .help-block{margin-top:13.2px}}.panel-body-loading{padding:0;text-align:center}.misago-footer{margin-bottom:50px}.misago-footer .noscript-message .material-icon{position:relative;bottom:1px;font-size:18px}.ui-preview{color:#eee;-webkit-animation:ui-preview-animation 1s linear infinite;-o-animation:ui-preview-animation 1s linear infinite;animation:ui-preview-animation 1s linear infinite}@keyframes ui-preview-animation{0%,100%{opacity:1;filter:alpha(opacity=100)}50%{opacity:.1;filter:alpha(opacity=10)}}.ui-preview-text{background:#eee;border-radius:100px;display:inline-block;height:14px;position:relative;top:3px}.ui-preview-paragraph .ui-preview-text{margin-right:6px}.ui-preview-paragraph .ui-preview-text:last-child{margin-right:0}.ui-preview-img{background:#eee;border-radius:5px}.pager-undercontent{margin-top:-20px}@media screen and (min-width:992px){.pager-more .btn{padding-left:20px;padding-right:20px}}.misago-pagination{overflow:auto}.misago-pagination .pagination{float:left;margin:0 10px 0 0}.misago-pagination .pagination li>a,.misago-pagination .pagination li>span{padding:2px}.misago-pagination .pagination .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.misago-pagination p{float:left;margin:0;padding:7px}.panel-poll h2{margin-top:0}.poll-select-choice .btn,.poll-select-choice .btn:active,.poll-select-choice .btn:focus,.poll-select-choice .btn:hover{background:0 0;border:transparent;-webkit-box-shadow:none;box-shadow:none;margin:6px 0;padding:0 0 0 6px;outline:0;text-align:left}.poll-select-choice .btn .material-icon{margin-right:6px;height:28px;width:28px;font-size:28px;line-heigh:28px;color:#eee}.poll-select-choice .btn.btn-selected .material-icon{color:#651fff}.poll-help{font-size:12px}.poll-chart-selected .material-icon{margin-right:4px;position:relative;bottom:1px;height:14px;width:14px;color:#43a047;font-size:14px;line-heigh:14px}.poll-options{margin-bottom:0}.user-status.user-banned .status-icon{color:#f44336}.user-status.user-online .status-icon{color:#43a047}.user-status.user-offline .status-icon{color:#777}.item-title,a.item-title:active,a.item-title:hover,a.item-title:link,a.item-title:visited{color:#212121;font-weight:700}.user-card-small-avatar img{width:100%;height:auto}@media screen and (min-width:768px){.user-card{text-align:center}.user-card-small-avatar{display:none}}@media screen and (max-width:767px){.poll-options{margin-top:-6px}.poll-options .btn{margin:6px 0}.user-card-avatar{display:none}}.toolbar{display:block;margin-bottom:20px}.toolbar.toolbar-bottom{margin-top:10px;margin-bottom:20px}.toolbar>h3{font-size:18px}.toolbar>p{padding:6px 0;text-align:center}@media screen and (min-width:992px){.toolbar-left{float:left;margin-right:16px}.toolbar-right{float:right;margin-left:16px}.toolbar .toolbar-bottom>.form-control,.toolbar .toolbar-control{margin:0;width:auto}.toolbar>h3,.toolbar>p{min-height:34px;margin:0;padding-top:6px;text-align:left}.toolbar>p{padding-top:7px}}.row-toolbar .toolbar-vertical-spacer{margin-top:10px}.row-toolbar-bottom-margin{margin-bottom:20px}@media screen and (min-width:768px){.row-toolbar p{padding-top:7px}}abbr{border:none!important}.item-title{text-decoration:none}a.item-title:hover{text-decoration:underline}.message-line{text-align:center}.message-line .material-icon{margin-right:6.67px;font-size:20px;line-height:20px;height:20px;width:20px}.misago-markup h1,.misago-markup h2,.misago-markup h3,.misago-markup h4,.misago-markup h5,.misago-markup h6{margin-top:40px}.misago-markup blockquote>*,.misago-markup>*{margin:20px 0}.misago-markup blockquote>:first-child,.misago-markup>:first-child{margin-top:0}.misago-markup blockquote>:last-child,.misago-markup>:last-child{margin-bottom:0}.misago-markup img{max-width:100%;max-height:500px}.misago-markup .quote-block,.misago-markup blockquote{background:#eee;border:none;font-size:14px}.misago-markup .quote-block .quote-heading,.misago-markup blockquote .quote-heading{padding:10px 20px;font-size:12px;font-weight:700}.misago-markup .quote-body{margin:0;padding:20px}.misago-markup .quote-body>.quote-block,.misago-markup .quote-body>blockquote{border:1px solid #dadada}.misago-markup ul,.misago-markup ul li{list-style-type:square}.misago-markup ol,.misago-markup ol li{list-style-type:decimal}.misago-markup pre{background:#eee;border:none;padding:10px;overflow:hidden;color:#000}.misago-markup pre code.hljs{margin:-10px;padding:9.5px}.modal-change-avatar .modal-avatar-index .avatar-preview{border-radius:6px;margin:0 auto;overflow:hidden;position:relative;width:200px;height:200px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader{display:none;position:absolute;top:50px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader .loader-spinning-wheel{border-width:10px;border-color:#fff transparent;width:100px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview.preview-loading img{opacity:.33;filter:alpha(opacity=33)}.modal-change-avatar .modal-avatar-index .avatar-preview.preview-loading .loader{display:block}@media (max-width:699px){.modal-change-avatar .modal-avatar-index .avatar-preview{margin-bottom:20px;width:150px;height:150px}.modal-change-avatar .modal-avatar-index .avatar-preview img{width:150px;height:150px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader{top:25px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader .loader-spinning-wheel{width:100px;height:100px}}.modal-change-avatar .modal-avatar-index .btn{text-align:left}.modal-change-avatar .modal-avatar-upload{text-align:center}.modal-change-avatar .modal-avatar-upload .btn-pick-file{background:0 0;border:2px solid #eee;border-radius:6px;padding:10px 24px;-webkit-box-shadow:none;box-shadow:none;color:#777;font-size:18px;text-align:center}.modal-change-avatar .modal-avatar-upload .btn-pick-file>.material-icon{display:block;margin:0 auto 13.2px;font-size:50px;width:50px;height:50px}.modal-change-avatar .modal-avatar-upload .btn-pick-file:active,.modal-change-avatar .modal-avatar-upload .btn-pick-file:hover{border-color:#777}.modal-change-avatar .modal-avatar-upload .text-muted{margin-top:13.2px}.modal-change-avatar .modal-avatar-upload .upload-progress img{border-radius:4px;margin-bottom:20px;max-height:80px;width:auto}.modal-change-avatar .modal-avatar-upload .upload-progress .progress{width:70%;margin:0 auto}.modal-avatar-crop .crop-form{margin:0 auto}.modal-avatar-crop .crop-form .cropit-image-zoom-input{margin-top:10px;-webkit-appearance:none;border:1px solid #fff;width:100%}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-webkit-slider-runnable-track{width:100%;height:8px;background:#eee;border:none;border-radius:3px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:20px;width:20px;border-radius:50%;background:#777;margin-top:-6px}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus{outline:0}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-webkit-slider-runnable-track{background:#eee}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-moz-range-track{width:100%;height:8px;background:#eee;border:none;border-radius:4px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-moz-range-thumb{border:none;height:20px;width:20px;border-radius:50%;background:#777}.modal-avatar-crop .crop-form .cropit-image-zoom-input:-moz-focusring{outline:#fff solid 1px;outline-offset:-1px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-track{width:100%;height:8px;background:0 0;border-color:transparent;border-width:8px 0;color:transparent}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-fill-lower{background:#eee;border-radius:16px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-fill-upper{background:#eee;border-radius:16px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-thumb{border:none;height:20px;width:20px;border-radius:50%;background:#777}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-ms-fill-lower{background:#eee}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-ms-fill-upper{background:#eee}.modal-change-avatar .modal-avatar-gallery{padding-bottom:0}.modal-change-avatar .modal-avatar-gallery .avatars-gallery{margin-bottom:20px}.modal-change-avatar .modal-avatar-gallery .avatars-gallery h3{margin-top:0}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .row{margin-bottom:10px}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn{border-radius:6px;border:2px solid #eee;background:0 0;padding:2px;position:relative}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:focus,.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:hover{border-color:#777}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn.avatar-selected,.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:active{border-color:#651fff}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn img{border-radius:4px;width:100%;height:auto}.category-main .read-status .material-icon{color:#eee}.category-main .read-status.item-new .material-icon{color:#651fff}.category-last-thread .media-heading a{display:inline-block;overflow:hidden;white-space:nowrap;width:290px;text-overflow:ellipsis;vertical-align:top}@media screen and (max-width:991px){.category-last-thread .media-heading a{width:275px}}@media screen and (max-width:767px){.category-last-thread .media-heading a{width:260px}}.category-thread-message .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.category-thread-message p{margin:0}.list-inline.subcategories-list{overflow:auto;margin-top:-10px}.list-inline.subcategories-list li{display:block;float:left}.list-inline.subcategories-list li a,.list-inline.subcategories-list li a:active,.list-inline.subcategories-list li a:focus,.list-inline.subcategories-list li a:hover,.list-inline.subcategories-list li a:link,.list-inline.subcategories-list li a:visited{background-color:#eee;border:1px solid #e2e2e2;border-radius:4px;display:inline-block;margin-top:10px;padding:6px 12px;color:#555}#posting-placeholder .first-row .form-control,.editor-border{border:1px solid #c8c8c8}.list-inline.subcategories-list li a:active,.list-inline.subcategories-list li a:hover,.list-inline.subcategories-list li:focus{background-color:#e2e2e2;color:#212121;text-decoration:none}.row.subcategories-list .btn{margin-top:20px;text-align:left}#posting-placeholder{background-color:#eee;display:none;margin-top:30px;margin-bottom:-30px;padding:20px 0;transition:height .3s}#posting-placeholder.slide-in{display:block}#posting-placeholder .first-row{margin-bottom:20px}#posting-placeholder .first-row .posting-options .btn{padding-top:4px;padding-bottom:4px}#posting-placeholder .first-row .posting-options .btn .btn-text{margin-left:5px;position:relative;top:1px}#posting-placeholder .first-row .posting-options .material-icon{width:14px;height:24px;margin-right:0;position:relative;top:5px;font-size:14px;line-height:14px;text-align:center}.posting-ui-preview{padding:20px 0;position:relative}.posting-ui-preview .form-control{box-shadow:none;resize:none}.posting-loader{text-align:center}.posting-loader .loader{height:100px}.posting-loader .loader .loader-spinning-wheel{width:100px;height:100px}.posting-message{text-align:center}.posting-message .material-icon{margin-right:6.67px;position:relative;top:-1px;width:28px;height:28px;font-size:28px;line-height:28px}.posting-message .message-body p{font-size:18px}.editor-border{background-color:#fff;border-radius:4px}.editor-border .form-control{border:none;resize:none}.editor-border .form-control,.editor-border .form-control:active,.editor-border .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.editor-footer{border-top:1px solid #c8c8c8;padding:6px 12px;overflow:auto}.editor-footer .pull-left{margin-right:12px}.editor-footer .pull-right{margin-left:12px}.editor-footer .btn-icon .material-icon{margin-bottom:-2px}@media screen and (max-width:991px){.editor-footer .buttons-list{float:none!important;margin:0 0 10px}.editor-footer .buttons-list .btn{display:inline-block;float:none!important;margin:6.67px}.editor-footer .btn-protect .btn-text{margin-left:10px}.editor-footer .btn-protect .material-icon{position:relative;bottom:2px;width:14px;height:14px;font-size:14px;line-height:14px}}@media screen and (min-width:768px) and (max-width:991px){.buttons-list .btn:first-child{margin-left:0}}@media screen and (max-width:767px){.buttons-list{text-align:center}.buttons-list .btn-protect{display:block;float:none!important;width:100%;margin:10px 0 0}}.editor-attachments-list{margin:0;padding:0}.editor-attachments-list li{margin:0}.editor-attachment-complete{border-top:1px solid #c8c8c8;padding:6px 12px 6px 0}.editor-attachment-complete .editor-attachment-image{float:left;width:50px}.editor-attachment-complete .editor-attachment-image a{background-size:cover;background-position:center;border-radius:3px;display:block;margin:0 auto;width:36px;height:36px}.editor-attachment-complete .editor-attachment-icon{float:left;width:50px;text-align:center}.editor-attachment-complete .editor-attachment-icon .material-icon{position:relative;top:2px;height:28px;width:28px;font-size:28px;line-height:28px}.editor-attachment-complete .editor-attachment-details{margin-left:50px}.editor-attachment-complete .editor-attachment-details h4,.editor-attachment-complete .editor-attachment-details p{margin:0;padding:0;font-size:14px}.editor-attachment-complete .editor-attachment-details p{margin-top:3px;color:#777;font-size:12px}@media screen and (min-width:768px){.editor-attachment-actions{padding-top:3px}}@media screen and (max-width:767px){.editor-attachment-actions{padding-left:12px;padding-right:12px}}.editor-attachment-error{border-top:1px solid #c8c8c8;padding:6px 12px 6px 0}.editor-attachment-error-icon{float:left;width:50px;text-align:center}.editor-attachment-error-icon .material-icon{position:relative;top:2px;height:28px;width:28px;font-size:28px;line-height:28px}.editor-attachment-error-message{margin-left:50px;padding:6px 0;position:relative}.editor-attachment-error-message h4,.editor-attachment-error-message p{margin:0;padding:0;font-size:14px}.editor-attachment-error-message p{margin-top:3px;font-size:12px}.editor-attachment-error-message .btn{position:absolute;top:9px;right:12px}@media screen and (max-width:767px){.editor-attachment-error-message .btn{display:block;margin-top:10px;position:static}}.editor-attachment-progress-bar{background:#c8c8c8;overflow:auto}.editor-attachment-progress{background:#651fff;float:left;height:1px}.editor-attachment-upload-message{margin:0;padding:6px 12px}#editor-upload-field{position:absolute;left:-1000px;top:-1000px}.atwho-view ul li img{border-radius:3px;margin-right:4.67px;width:20px;height:20px}.participant-card .btn-user,.participant-card .dropdown.open .btn-user{margin-bottom:20px}.participant-card .btn-user,.participant-card .btn-user:focus,.participant-card .btn-user:focus:active,.participant-card .btn-user:hover,.participant-card .dropdown.open .btn-user,.participant-card .dropdown.open .btn-user:focus,.participant-card .dropdown.open .btn-user:focus:active,.participant-card .dropdown.open .btn-user:hover{padding:0;overflow:hidden;text-align:left}.participant-card .btn-user img,.participant-card .dropdown.open .btn-user img{background-color:#fff;width:34px;height:34px;margin-right:8px}.panel-participants p{margin:7px 0 0}.poll-choices-control .list-group-item{padding:0}.poll-choices-control .list-group-item .btn{background:0 0;border:transparent;float:left;margin:0 2px -29px;padding:0;width:28px;height:28px;position:relative;top:3px}.poll-choices-control .list-group-item .btn .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.poll-choices-control input,.poll-choices-control input:active,.poll-choices-control input:focus{background:0 0;border:none;-webkit-box-shadow:none;box-shadow:none;outline:0;margin-left:30px;padding:6px 12px;width:100%}.posts-list{margin:0;padding:0;clear:both}.posts-list li{list-style:none;margin:0;padding:0}.post-side{font-size:12px}.post-side .media{margin:0}.post-side .poster-avatar{height:36px;width:36px}@media screen and (min-width:992px){.post-side .poster-avatar{margin-top:4px;height:82px;width:82px}}.post-side .media-heading{display:block;margin:-1px 0 0;font-size:14px}.post-side .media-heading .user-status{margin-left:2px}.post-heading .pull-right,.post-side .pull-right{margin-left:16px}@media screen and (min-width:992px){.post-side .media-heading{margin-top:3px;font-size:18px}.post-side .media-heading .user-status{display:none}.post-side .user-title{margin-top:4px;margin-bottom:5px}.post-side .user-postcount,.post-side .user-status{display:block}.post-side .pull-right{display:none}}.post-heading{height:36px}@media screen and (max-width:991px){.post-heading{margin-top:10px}.post-heading .pull-right{display:none}}.post-heading .label{margin-top:6px;font-size:14px;font-weight:400}.post-heading .label-unread{color:#fff}.post-body{padding-top:20px;padding-bottom:30px}.post-status-message{overflow:auto}.post-status-message .material-icon{float:left;font-size:28px;line-height:28px}.post-status-message p{margin:4px 0 0 36px}.post-status-best-answer{background-color:#43a047;color:#fff}.post-status-hidden{background-color:#f44336;color:#fff}.post-status-unapproved{background-color:#651fff;color:#fff}.post-status-protected{background-color:#555;color:#fff}.post-footer .pull-left{margin-right:16px}.post-footer .pull-right{margin-left:16px}.post-footer p{padding-top:7px;padding-bottom:6px;margin-bottom:0}.post-attachments{padding:6px 16px}.post-attachments .row>div{margin:10px 0}.post-attachments .post-attachment-preview{float:left;height:40px;width:40px;text-align:center}.post-attachments .post-attachment{margin-left:52px}.post-attachments .post-thumbnail{display:block;background-size:cover;background-position:center;border-radius:3px;width:40px;height:40px}.post-attachments .material-icon{width:28px;height:28px;position:relative;top:5px;font-size:28px;line-height:28px}.post-attachments .material-icon:active,.post-attachments .material-icon:focus,.post-attachments .material-icon:hover,.post-attachments .material-icon:link,.post-attachments .material-icon:visited{color:#222;text-decoration:none}.post-attachments .post-attachment-description{margin:0;padding:0;color:#777;font-size:12px}.post-feed .post-side .media-heading{margin:0;font-size:14px}.post-feed .post-side .user-title{margin:0;font-size:12px}.post-feed .post-side .btn{display:inline-block}.post-feed .post-side img{margin-top:0;width:36px;height:36px}.post-feed .post-heading{height:auto;margin:10px 0}.post-feed .post-heading .btn{margin-right:16px;max-width:100%;text-align:left;white-space:normal}.posts-list .event{margin-bottom:20px;color:#777}.posts-list .event .media{margin-top:5px}.posts-list .event-label .label-unread{background-color:#43a047;color:#fff}.posts-list .event .text-right{padding-right:0;text-align:right}.posts-list .event .text-right .material-icon{margin-right:-12px;height:28px;width:28px;font-size:28px;line-height:28px;text-align:center}.posts-list .event .text-left{padding-left:24px}.posts-list .event .event-message{margin-bottom:5px;font-size:18px}.posts-list .event .event-info{margin:0;font-size:12px}.posts-list .event .event-info li{margin-right:12px}.posts-list .event .event-info li:last-child{margin-right:0}.posts-list .event .event-controls .btn-link{border:0;margin:0 12px 0 0;padding:0;font-size:12px}.posts-list .event .event-controls .btn-link:last-child{margin-right:0}@media screen and (max-width:767px){.posts-list .event .text-right{width:28px;text-align:right}.posts-list .event .text-left:first-child{padding-left:0}.posts-list .event .event-info{margin-top:5px}.posts-list .event .event-controls{clear:both;margin-top:5px}.posts-list .event .event-controls .btn-link{margin-right:20px;font-size:14px}.posts-list .event .event-controls .btn-link:last-child{margin-right:0}.post-changelog-toolbar .post-change-label{text-align:center}}.post-changelog-diff{padding:0;margin:0}.post-changelog-diff .list-unstyled{padding:0;margin:5px 0}.post-changelog-diff .diff-item{padding:5px 10px}.post-changelog-diff .diff-item-sub{color:#f44336}.post-changelog-diff .diff-item-add{color:#43a047}.post-changelog-toolbar .row{margin-left:-12px;margin-right:-12px}@media screen and (max-width:767px){.page-error .message-panel,.page-message .message-panel{text-align:center}.page-error .message-icon,.page-message .message-icon{margin:30px;font-size:80px}}@media screen and (min-width:768px){.page-error .message-panel,.page-message .message-panel{margin:60px auto;max-width:779.35px;overflow:auto}.page-error .message-icon,.page-message .message-icon{float:left}.page-error .message-icon .material-icon,.page-message .message-icon .material-icon{font-size:80px}.page-error .message-body,.page-message .message-body{margin-top:16px;margin-left:100px;font-size:18px}.page-error .message-body p.lead,.page-message .message-body p.lead{font-size:36px}}.threads-list{margin-bottom:20px}.threads-list .thread-title,.threads-list .thread-title:active,.threads-list .thread-title:focus,.threads-list .thread-title:hover,.threads-list .thread-title:link,.threads-list .thread-title:visited{font-size:18px;font-weight:400}.thread-last-action .media-body,.thread-last-action .media-left{padding-top:2px}.threads-list .thread-options{padding-top:5px}.threads-list .thread-main .media-left{padding-top:2px;padding-bottom:1px}.threads-list .thread-details-top{overflow-x:auto;white-space:nowrap}.threads-list .thread-details-bottom div>a,.threads-list .thread-details-bottom div>span,.threads-list .thread-details-top>a,.threads-list .thread-details-top>span{margin-right:16px;font-size:12px;font-weight:400}.threads-list .thread-details-bottom div>a>.material-icon,.threads-list .thread-details-bottom div>span>.material-icon,.threads-list .thread-details-top>a>.material-icon,.threads-list .thread-details-top>span>.material-icon{position:relative;top:-1px}.threads-list .thread-details-bottom div>a .detail-text,.threads-list .thread-details-bottom div>span .detail-text,.threads-list .thread-details-top>a .detail-text,.threads-list .thread-details-top>span .detail-text{margin-left:2px}@media screen and (max-width:991px){.threads-list .thread-details-bottom div>a,.threads-list .thread-details-bottom div>span,.threads-list .thread-details-top>a,.threads-list .thread-details-top>span{margin-right:12px}}@media screen and (max-width:767px){.threads-list .thread-details-bottom{margin-top:8px}.threads-list .thread-details-top{margin-bottom:6px}}@media screen and (min-width:768px){.threads-list .thread-details-top{margin-left:50px}}.thread-last-action{padding-top:1px}.thread-last-action .thread-last-poster{display:block}.thread-options-xs{margin-top:-10px;margin-bottom:-20px;position:relative;top:5px}.thread-options-xs .btn{padding:0 2px;font-size:12px}.thread-options-xs .btn .material-icon{width:14px;height:14px;font-size:14px;line-height:14px}.threads-diff-message{padding:0}.threads-diff-message .btn{border-bottom-left-radius:0;border-bottom-right-radius:0;padding:10px 16px;width:100%;overflow:none;text-align:left}.threads-diff-message .btn .material-icon{margin-right:4px;width:24px;font-size:24px;height:24px}@media screen and (max-width:991px){.threads-diff-message .btn{text-align:center;white-space:normal;word-wrap:break-word}.threads-diff-message .btn .material-icon{display:none}}.threads-list .thread-preview .ui-preview-text{margin-right:16px}.threads-list .thread-preview .thread-details-bottom div .ui-preview-text,.threads-list .thread-preview .thread-details-top .ui-preview-text{height:9.6px}.threads-list .thread-preview .thread-details-bottom{margin-top:6px}.threads-list .thread-preview .thread-details-top{margin-bottom:6px}.threads-list .thread-busy .thread-row{-webkit-animation:thread-busy-animation .6s linear infinite;-o-animation:thread-busy-animation .6s linear infinite;animation:thread-busy-animation .6s linear infinite}@keyframes thread-busy-animation{0%,100%{opacity:.2;filter:alpha(opacity=20)}50%{opacity:.5;filter:alpha(opacity=50)}}.btn-danger.disabled,.btn-danger.disabled:active,.btn-danger.disabled:hover,.btn-danger:disabled,.btn-danger:disabled:hover,.btn-default.btn-outline.disabled,.btn-default.btn-outline.disabled:active,.btn-default.btn-outline.disabled:hover,.btn-default.btn-outline:disabled,.btn-default.btn-outline:disabled:hover,.btn-default.disabled,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:disabled,.btn-default:disabled:hover,.btn-primary.disabled,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:disabled,.btn-primary:disabled:hover,.btn-success.disabled,.btn-success.disabled:active,.btn-success.disabled:hover,.btn-success:disabled,.btn-success:disabled:hover,.navbar-misago .btn-register.disabled,.navbar-misago .btn-register.disabled:active,.navbar-misago .btn-register.disabled:hover,.navbar-misago .btn-register:disabled,.navbar-misago .btn-register:disabled:hover,.navbar-misago .btn-sign-in.disabled,.navbar-misago .btn-sign-in.disabled:active,.navbar-misago .btn-sign-in.disabled:hover,.navbar-misago .btn-sign-in:disabled,.navbar-misago .btn-sign-in:disabled:hover,.page-header .btn-outline.btn-default.disabled,.page-header .btn-outline.btn-default.disabled:active,.page-header .btn-outline.btn-default.disabled:hover,.page-header .btn-outline.btn-default:disabled,.page-header .btn-outline.btn-default:disabled:hover,.page-header .btn-outline.btn-primary.disabled,.page-header .btn-outline.btn-primary.disabled:active,.page-header .btn-outline.btn-primary.disabled:hover,.page-header .btn-outline.btn-primary:disabled,.page-header .btn-outline.btn-primary:disabled:hover,.threads-diff-message .btn.disabled,.threads-diff-message .btn.disabled:active,.threads-diff-message .btn.disabled:hover,.threads-diff-message .btn:disabled,.threads-diff-message .btn:disabled:hover{opacity:.25;filter:alpha(opacity=25)}.active-posters li{display:block;overflow:auto}.active-posters .rank-user-avatar{float:left}.active-posters .rank-user{float:left;margin-top:3px}@media screen and (max-width:991px){.active-posters .rank-user-avatar{height:42px}.active-posters .rank-user-avatar img{width:36px;height:36px;position:relative;top:3px}.active-posters .rank-user{margin-left:13.2px;width:40%}}@media screen and (max-width:767px){.active-posters .rank-user{float:none;margin-left:50px;width:auto}.active-posters .user-details{margin-top:3px}.active-posters .user-details .rank-name,.active-posters .user-details .user-title{font-weight:400}}@media screen and (min-width:992px){.active-posters .rank-user{margin-left:16px;width:25%}.active-posters .rank-user .user-name{font-size:18px}}.active-posters .user-details{overflow:auto;font-family:Sans-Serif}.active-posters .user-details .rank-name,.active-posters .user-details .user-status,.active-posters .user-details .user-title{display:block;float:left;margin-right:3px;font-size:12px}@media screen and (min-width:992px){.active-posters .user-details .rank-name,.active-posters .user-details .user-status,.active-posters .user-details .user-title{margin-right:8px}.active-posters .user-details{overflow:visible}.active-posters .user-details .rank-name,.active-posters .user-details .user-title{height:14px;overflow:hidden;position:relative;top:1px;vertical-align:baseline}}.active-posters .user-details .user-title{margin-right:0}.active-posters .user-status{overflow:auto;position:relative;top:1px}.active-posters .user-status span{display:block;float:left}@media screen and (min-width:992px){.active-posters .user-status{height:14px;overflow:hidden}}.active-posters .user-status .status-icon{position:relative}@media screen and (max-width:991px){.active-posters .user-status .status-icon{top:0;width:12px;height:12px;font-size:12px;line-height:12px}}@media screen and (min-width:992px){.active-posters .user-status .status-icon{top:1px;margin-right:3px;width:13px;height:13px;font-size:13px;line-height:13px}}.active-posters .rank-name .ui-preview-text,.active-posters .status-label.ui-preview-text,.active-posters .user-title .ui-preview-text{height:11px;position:relative;top:2px;font-size:11px;line-height:11px}.active-posters .rank-name .ui-preview-text,.active-posters .user-title .ui-preview-text{position:static}.active-posters .rank-position small,.active-posters .rank-position strong,.active-posters .rank-posts-counted small,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total small,.active-posters .rank-posts-total strong{display:block}@media screen and (max-width:991px){.active-posters .rank-position,.active-posters .rank-posts-counted,.active-posters .rank-posts-total{overflow:auto}.active-posters .rank-position small,.active-posters .rank-position strong,.active-posters .rank-posts-counted small,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total small,.active-posters .rank-posts-total strong{float:left;font-size:10.5px}.active-posters .rank-position strong,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total strong{min-width:30px;margin-right:3px;text-align:right}.active-posters .rank-position .ui-preview-text,.active-posters .rank-posts-counted .ui-preview-text,.active-posters .rank-posts-total .ui-preview-text{height:8px;position:relative;top:-1px;font-size:8px;line-height:8px}}@media screen and (min-width:992px){.active-posters .rank-position,.active-posters .rank-posts-counted,.active-posters .rank-posts-total{float:left;margin-top:3px;width:23%;font-size:18px;text-align:center}.active-posters .rank-position small,.active-posters .rank-posts-counted small,.active-posters .rank-posts-total small{font-size:12px;font-weight:400}}.user-compact-stats .rank-position small,.user-compact-stats .rank-position strong,.user-compact-stats .rank-posts-counted small,.user-compact-stats .rank-posts-counted strong{display:inline-block;float:none}.user-compact-stats .rank-position strong,.user-compact-stats .rank-posts-counted strong{min-width:auto}.user-compact-stats .rank-position small,.user-compact-stats .rank-posts-counted small{margin-right:20px}@media screen and (min-width:768px) and (max-width:991px){.active-posters .rank-position{margin-top:6px}.active-posters .rank-posts-total{display:none}}.page-user-profile .page-header{padding-top:20px}.page-user-profile .page-header .alert{margin-bottom:20px;text-align:center}.page-user-profile .page-header h1{margin-bottom:0}.page-user-profile .page-header .btn-aligned{margin-top:0}@media screen and (max-width:767px){.page-user-profile .page-header,.page-user-profile .page-header .header-stats ul{text-align:center}.page-user-profile .page-header img{clear:both;margin-top:20px;width:100px;height:100px}.page-user-profile .page-header h1{margin-top:20px;font-size:28px}.page-user-profile .page-header .header-stats ul li{display:inline-block;white-space:nowrap}.page-user-profile .profile-side-avatar{display:none}}@media screen and (min-width:768px) and (max-width:991px){.page-user-profile .page-header h1,.page-user-profile .page-header img{float:left}.page-user-profile .page-header img{margin-top:20px;margin-right:24px;width:80px;height:80px}.page-user-profile .page-header h1{position:relative;top:25px}.page-user-profile .page-header .header-stats{margin-left:112px;margin-top:25px;margin-bottom:-45px;position:relative;bottom:50px}}@media screen and (min-width:992px){.page-user-profile .page-header .user-avatar-sm{display:none}.page-user-profile .page-header h1{position:relative;top:5px}.page-user-profile .page-header .header-stats ul li{display:block;float:left}.page-user-profile .profile-side-avatar img{width:100%;height:auto;margin-top:-140px;margin-bottom:20px}.username-history li{display:block;overflow:auto}}.username-history .change-avatar{float:left}.username-history .change-avatar a,.username-history .change-avatar span{margin-right:10px}.username-history .change-avatar a img,.username-history .change-avatar span img{width:42px;height:42px}.username-history .change{min-height:20px;overflow:auto}.username-history .change span{display:block;float:left}.username-history .change .material-icon{margin:0 7px;position:relative;top:4px}.search-footer p{margin-top:20px;color:#555;font-size:12px;text-align:center}@media screen and (min-width:768px){.page-search-form{padding-top:40px}}@media screen and (min-width:992px){.username-history .change-avatar a img,.username-history .change-avatar span img{width:18px;height:18px;position:relative;bottom:1px}.username-history .change-author{float:left;width:30%}.username-history .change{float:left;width:40%}.username-history .change-date{float:left;width:20%}.page-search-form{padding-bottom:40px}.page-search-form h1{position:relative;top:5px}.page-search-form .form-group{margin-bottom:0}}.hljs{display:block;overflow-x:auto;padding:.5em;background:#eee;color:#000}.hljs-addition,.hljs-attribute,.hljs-emphasis,.hljs-link{color:#070}.hljs-emphasis{font-style:italic}.hljs-deletion,.hljs-string,.hljs-strong{color:#d14}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-section,.hljs-title{color:#900}.hljs-class .hljs-title,.hljs-type{color:#458}.hljs-template-variable,.hljs-variable{color:#369}.hljs-bullet{color:#970}.hljs-meta{color:#34b}.hljs-code,.hljs-keyword,.hljs-literal,.hljs-number,.hljs-selector-tag{color:#099}.hljs-regexp{background-color:#fff0ff;color:#808}.hljs-symbol{color:#990073}.hljs-name,.hljs-selector-class,.hljs-selector-id,.hljs-tag{color:#070}.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400}#misago-container,body,html{background:#fff}abbr{outline:0;text-decoration:none}.shadow-2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.shadow-3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.04),0 3px 3px -2px rgba(0,0,0,.06),0 1px 8px 0 rgba(0,0,0,.12)}.shadow-4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.shadow-6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.04),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.06)}.shadow-8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.04),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.06)}.shadow-16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.04),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.06)}.shadow-24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.04),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.06)}.btn-default,.btn-default.disabled,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:disabled,.btn-default:disabled:hover{background:#f5f5f5;border:1px solid #f5f5f5;color:#757575;-webkit-box-shadow:none;box-shadow:none}.btn-default:focus,.btn-default:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default:active,.btn-default:active:focus,.dropdown.open .btn-default,.dropdown.open .btn-default:active:focus,.dropdown.open .btn-default:focus,.dropdown.open .btn-default:hover{background:#bdbdbd;border:1px solid #bdbdbd;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-loading,.btn-default.btn-loading:active,.btn-default.btn-loading:active:focus,.btn-default.btn-loading:disabled,.btn-default.btn-loading:disabled:hover,.btn-default.btn-loading:focus,.btn-default.btn-loading:hover{color:transparent}.btn-primary,.btn-primary.disabled,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:disabled,.btn-primary:disabled:hover{background:#a36eff;border:1px solid #a36eff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary:focus,.btn-primary:hover{background:#b388ff;border:1px solid #b388ff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary:active,.btn-primary:active:focus,.dropdown.open .btn-primary,.dropdown.open .btn-primary:active:focus,.dropdown.open .btn-primary:focus,.dropdown.open .btn-primary:hover{background:#823bff;border:1px solid #823bff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-loading,.btn-primary.btn-loading:active,.btn-primary.btn-loading:active:focus,.btn-primary.btn-loading:disabled,.btn-primary.btn-loading:disabled:hover,.btn-primary.btn-loading:focus,.btn-primary.btn-loading:hover{color:transparent}.btn-success,.btn-success.disabled,.btn-success.disabled:active,.btn-success.disabled:hover,.btn-success:disabled,.btn-success:disabled:hover{background:#00c853;border:1px solid #00c853;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success:focus,.btn-success:hover{background:#00af48;border:1px solid #00af48;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success:active,.btn-success:active:focus,.dropdown.open .btn-success,.dropdown.open .btn-success:active:focus,.dropdown.open .btn-success:focus,.dropdown.open .btn-success:hover{background:#007c33;border:1px solid #007c33;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success.btn-loading,.btn-success.btn-loading:active,.btn-success.btn-loading:active:focus,.btn-success.btn-loading:disabled,.btn-success.btn-loading:disabled:hover,.btn-success.btn-loading:focus,.btn-success.btn-loading:hover{color:transparent}.btn-danger,.btn-danger.disabled,.btn-danger.disabled:active,.btn-danger.disabled:hover,.btn-danger:disabled,.btn-danger:disabled:hover{background:#ef5350;border:1px solid #ef5350;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger:focus,.btn-danger:hover{background:#ff8a80;border:1px solid #ff8a80;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger:active,.btn-danger:active:focus,.dropdown.open .btn-danger,.dropdown.open .btn-danger:active:focus,.dropdown.open .btn-danger:focus,.dropdown.open .btn-danger:hover{background:#d32f2f;border:1px solid #d32f2f;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger.btn-loading,.btn-danger.btn-loading:active,.btn-danger.btn-loading:active:focus,.btn-danger.btn-loading:disabled,.btn-danger.btn-loading:disabled:hover,.btn-danger.btn-loading:focus,.btn-danger.btn-loading:hover{color:transparent}.btn-default.btn-outline,.btn-default.btn-outline.disabled,.btn-default.btn-outline.disabled:active,.btn-default.btn-outline.disabled:hover,.btn-default.btn-outline:disabled,.btn-default.btn-outline:disabled:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:#616161;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline:focus,.btn-default.btn-outline:hover{background:#bdbdbd;border:1px solid #bdbdbd;color:#424242;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline:active,.btn-default.btn-outline:active:focus,.dropdown.open .btn-default.btn-outline,.dropdown.open .btn-default.btn-outline:active:focus,.dropdown.open .btn-default.btn-outline:focus,.dropdown.open .btn-default.btn-outline:hover{background:#eee;border:1px solid #eee;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline.btn-loading,.btn-default.btn-outline.btn-loading:active,.btn-default.btn-outline.btn-loading:active:focus,.btn-default.btn-outline.btn-loading:disabled,.btn-default.btn-outline.btn-loading:disabled:hover,.btn-default.btn-outline.btn-loading:focus,.btn-default.btn-outline.btn-loading:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline,.btn-primary.btn-outline.disabled,.btn-primary.btn-outline.disabled:active,.btn-primary.btn-outline.disabled:hover,.btn-primary.btn-outline:disabled,.btn-primary.btn-outline:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline.disabled,.btn-primary.btn-outline.disabled:active,.btn-primary.btn-outline.disabled:hover,.btn-primary.btn-outline:disabled,.btn-primary.btn-outline:disabled:hover{opacity:.25;filter:alpha(opacity=25)}.btn-primary.btn-outline:focus,.btn-primary.btn-outline:hover{background:#651fff;border:1px solid #651fff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline:active,.btn-primary.btn-outline:active:focus,.dropdown.open .btn-primary.btn-outline,.dropdown.open .btn-primary.btn-outline:active:focus,.dropdown.open .btn-primary.btn-outline:focus,.dropdown.open .btn-primary.btn-outline:hover{background:#6200ea;border:1px solid #6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline.btn-loading,.btn-primary.btn-outline.btn-loading:active,.btn-primary.btn-outline.btn-loading:active:focus,.btn-primary.btn-outline.btn-loading:disabled,.btn-primary.btn-outline.btn-loading:disabled:hover,.btn-primary.btn-outline.btn-loading:focus,.btn-primary.btn-outline.btn-loading:hover{background:#7c4dff;border:1px solid #7c4dff;color:transparent;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in{border-radius:100px}.navbar-misago .btn-sign-in,.navbar-misago .btn-sign-in.disabled,.navbar-misago .btn-sign-in.disabled:active,.navbar-misago .btn-sign-in.disabled:hover,.navbar-misago .btn-sign-in:disabled,.navbar-misago .btn-sign-in:disabled:hover{background:0 0;border:1px solid transparent;color:#e0e0e0;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in:focus,.navbar-misago .btn-sign-in:hover{background:#666060;border:1px solid #666060;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .navbar-misago .btn-sign-in,.dropdown.open .navbar-misago .btn-sign-in:active:focus,.dropdown.open .navbar-misago .btn-sign-in:focus,.dropdown.open .navbar-misago .btn-sign-in:hover,.navbar-misago .btn-sign-in:active,.navbar-misago .btn-sign-in:active:focus{background:#424242;border:1px solid #424242;color:#fff;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in.btn-loading,.navbar-misago .btn-sign-in.btn-loading:active,.navbar-misago .btn-sign-in.btn-loading:active:focus,.navbar-misago .btn-sign-in.btn-loading:disabled,.navbar-misago .btn-sign-in.btn-loading:disabled:hover,.navbar-misago .btn-sign-in.btn-loading:focus,.navbar-misago .btn-sign-in.btn-loading:hover{background:0 0;border:1px solid transparent;color:transparent;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register{border-radius:100px}.navbar-misago .btn-register,.navbar-misago .btn-register.disabled,.navbar-misago .btn-register.disabled:active,.navbar-misago .btn-register.disabled:hover,.navbar-misago .btn-register:disabled,.navbar-misago .btn-register:disabled:hover{background:0 0;border:1px solid #757575;color:#e0e0e0;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register:focus,.navbar-misago .btn-register:hover{background:#fff;border:1px solid #fff;color:#212121;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .navbar-misago .btn-register,.dropdown.open .navbar-misago .btn-register:active:focus,.dropdown.open .navbar-misago .btn-register:focus,.dropdown.open .navbar-misago .btn-register:hover,.navbar-misago .btn-register:active,.navbar-misago .btn-register:active:focus{background:#bdbdbd;border:1px solid #bdbdbd;color:#212121;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register.btn-loading,.navbar-misago .btn-register.btn-loading:active,.navbar-misago .btn-register.btn-loading:active:focus,.navbar-misago .btn-register.btn-loading:disabled,.navbar-misago .btn-register.btn-loading:disabled:hover,.navbar-misago .btn-register.btn-loading:focus,.navbar-misago .btn-register.btn-loading:hover{background:0 0;border:1px solid #757575;color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-social-battlenet,.btn-social-battlenet-oauth2,.btn-social-battlenet-oauth2:disabled,.btn-social-battlenet-oauth2:disabled:hover,.btn-social-battlenet:disabled,.btn-social-battlenet:disabled:hover{color:#0e86ca;font-weight:700}.btn-social-bungie,.btn-social-bungie:disabled,.btn-social-bungie:disabled:hover{color:#0096db;font-weight:700}.btn-social-facebook,.btn-social-facebook-app,.btn-social-facebook-app:disabled,.btn-social-facebook-app:disabled:hover,.btn-social-facebook:disabled,.btn-social-facebook:disabled:hover{color:#3b5998;font-weight:700}.btn-social-github,.btn-social-github-enterprise,.btn-social-github-enterprise-org,.btn-social-github-enterprise-org:disabled,.btn-social-github-enterprise-org:disabled:hover,.btn-social-github-enterprise-team,.btn-social-github-enterprise-team:disabled,.btn-social-github-enterprise-team:disabled:hover,.btn-social-github-enterprise:disabled,.btn-social-github-enterprise:disabled:hover,.btn-social-github-team,.btn-social-github-team:disabled,.btn-social-github-team:disabled:hover,.btn-social-github:disabled,.btn-social-github:disabled:hover{color:#000;font-weight:700}.btn-social-gitlab,.btn-social-gitlab:disabled,.btn-social-gitlab:disabled:hover{color:#fc6d26;font-weight:700}.btn-social-google,.btn-social-google-oauth,.btn-social-google-oauth2,.btn-social-google-oauth2:disabled,.btn-social-google-oauth2:disabled:hover,.btn-social-google-oauth:disabled,.btn-social-google-oauth:disabled:hover,.btn-social-google-openidconnect,.btn-social-google-openidconnect:disabled,.btn-social-google-openidconnect:disabled:hover,.btn-social-google-plus,.btn-social-google-plus:disabled,.btn-social-google-plus:disabled:hover,.btn-social-google:disabled,.btn-social-google:disabled:hover{color:#dd4b39;font-weight:700}.btn-social-linkedin,.btn-social-linkedin:disabled,.btn-social-linkedin:disabled:hover{color:#0077b5;font-weight:700}.btn-social-steam,.btn-social-steam:disabled,.btn-social-steam:disabled:hover{color:#5c7e10;font-weight:700}.btn-social-twitter,.btn-social-twitter:disabled,.btn-social-twitter:disabled:hover{color:#1da1f2;font-weight:700}.form-social-auth .row{margin-top:-6px;margin-bottom:-6px}.form-social-auth .btn{margin:6px 0}input.form-control,textarea.form-control{border-color:#ccc;box-shadow:inset 0 0 0 1px #ccc}input.form-control:focus,textarea.form-control:focus{border-color:#66afe9;box-shadow:inset 0 0 0 1px #66afe9}.has-error input.form-control{border-color:#f44336;box-shadow:inset 0 0 0 1px #f44336}.has-error input.form-control:focus{border-color:#f99d97;box-shadow:inset 0 0 0 1px #f99d97}.has-success input.form-control{border-color:#00c853;box-shadow:inset 0 0 0 1px #00c853}.has-success input.form-control:focus{border-color:#2fff85;box-shadow:inset 0 0 0 1px #2fff85}.password-strength{margin-top:10px}.password-strength .text-small{margin-top:4px;color:#616161;font-size:12px}.password-strength .progress{margin:0}.auth-message{background:#651fff;padding:80px 0;box-shadow:0 8px 10px 1px rgba(0,0,0,.04),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.06)}.auth-message p{color:#fff}.auth-message .btn{background:#4527a0;border-color:#4527a0;color:#fff}.auth-message .btn:focus,.auth-message .btn:hover{background:#b388ff;border-color:#b388ff;color:#fff}.auth-message .btn:active{background:#fff;border-color:#fff;color:#651fff}.navbar-misago,.page-header{border-bottom:none}.dropdown-menu{border:none;box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.user-dropdown .dropdown-header strong{display:block;font-size:18px;font-weight:300}.user-dropdown .dropdown-header .user-stats{margin-top:10px;font-size:12px}.user-dropdown .dropdown-header .user-stats .material-icon{margin-right:3px;position:relative;bottom:1px;width:14px;height:14px;font-size:14px}.user-dropdown .guest-preview{padding-bottom:10px}.dropdown-search-loader,.dropdown-search-message{padding:10px 16px;border-top:1px solid #eee}.user-dropdown .badge{background-color:#f44336}.user-dropdown .btn-link:active .badge,.user-dropdown .btn-link:focus .badge,.user-dropdown .btn-link:hover .badge,.user-dropdown a:active .badge,.user-dropdown a:focus .badge,.user-dropdown a:hover .badge{background-color:#fff;color:#f44336}.mobile-dropdown.open{margin:0}.navbar-misago .user-avatar{background:#fff;border-radius:3px}.navbar-misago .brand-link img{border-radius:3px}.dropdown-search-results{border-radius:0 0 4px 4px}.dropdown-search-message{color:#777}.dropdown-search-header{border-top:1px solid #eee;padding:8px 20px;color:#777;font-weight:700}.dropdown-search-thread small,.dropdown-search-user small{color:#777}.dropdown-search-thread:focus small,.dropdown-search-thread:hover small,.dropdown-search-user:focus small,.dropdown-search-user:hover small{color:#FAFAFA}.dropdown-search-thread:active small,.dropdown-search-user:active small{color:#fff}.dropdown-search-thread .dropdown-search-post-content{overflow:hidden;white-space:normal;max-height:47px}.dropdown-search-thread .dropdown-search-post-footer{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dropdown-search-footer{padding-bottom:5px}.dropdown-search-footer:last-child{padding-bottom:0}.dropdown-menu>li.dropdown-search-footer>a{font-size:small}.page-header{background:0 0}.page-header-bg{background:url(../img/page-header.svg) #6200ea;background-size:cover;min-height:8px}@media screen and (min-width:992px){.page-header-bg{margin-bottom:40px}}.page-header h1{color:#fff;font-weight:400}.page-header .btn-outline.btn-default,.page-header .btn-outline.btn-default.disabled,.page-header .btn-outline.btn-default.disabled:active,.page-header .btn-outline.btn-default.disabled:hover,.page-header .btn-outline.btn-default:disabled,.page-header .btn-outline.btn-default:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-default:focus,.page-header .btn-outline.btn-default:hover{background:#651fff;border:1px solid #651fff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .page-header .btn-outline.btn-default,.dropdown.open .page-header .btn-outline.btn-default:active:focus,.dropdown.open .page-header .btn-outline.btn-default:focus,.dropdown.open .page-header .btn-outline.btn-default:hover,.page-header .btn-outline.btn-default:active,.page-header .btn-outline.btn-default:active:focus{background:#6200ea;border:1px solid #6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-default.btn-loading,.page-header .btn-outline.btn-default.btn-loading:active,.page-header .btn-outline.btn-default.btn-loading:active:focus,.page-header .btn-outline.btn-default.btn-loading:disabled,.page-header .btn-outline.btn-default.btn-loading:disabled:hover,.page-header .btn-outline.btn-default.btn-loading:focus,.page-header .btn-outline.btn-default.btn-loading:hover{color:transparent}.page-header .btn-outline.btn-primary,.page-header .btn-outline.btn-primary.disabled,.page-header .btn-outline.btn-primary.disabled:active,.page-header .btn-outline.btn-primary.disabled:hover,.page-header .btn-outline.btn-primary:disabled,.page-header .btn-outline.btn-primary:disabled:hover{background:#f5f5f5;border:1px solid #f5f5f5;color:#424242;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-primary:focus,.page-header .btn-outline.btn-primary:hover{background:#fff;border:1px solid #fff;color:#212121;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .page-header .btn-outline.btn-primary,.dropdown.open .page-header .btn-outline.btn-primary:active:focus,.dropdown.open .page-header .btn-outline.btn-primary:focus,.dropdown.open .page-header .btn-outline.btn-primary:hover,.page-header .btn-outline.btn-primary:active,.page-header .btn-outline.btn-primary:active:focus{background:#212121;border:1px solid #212121;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-primary.btn-loading,.page-header .btn-outline.btn-primary.btn-loading:active,.page-header .btn-outline.btn-primary.btn-loading:active:focus,.page-header .btn-outline.btn-primary.btn-loading:disabled,.page-header .btn-outline.btn-primary.btn-loading:disabled:hover,.page-header .btn-outline.btn-primary.btn-loading:focus,.page-header .btn-outline.btn-primary.btn-loading:hover{color:transparent}.header-stats{color:#ede7f6}.header-stats li a,.header-stats li a:link,.header-stats li a:visited{color:#fff}.header-stats li a:active,.header-stats li a:focus,.header-stats li a:hover{color:#f5f5f5}.header-stats li .user-status.user-banned .status-icon,.header-stats li .user-status.user-offline .status-icon,.header-stats li .user-status.user-online .status-icon{color:#ede7f6}.header-stats li .item-title{color:#fff!important}.page-header .go-back-sm,.page-header .go-back-sm:link,.page-header .go-back-sm:visited{color:#ede7f6}.page-header .go-back-sm:active,.page-header .go-back-sm:focus,.page-header .go-back-sm:hover{color:#f5f5f5}.page-header .breadcrumb,.page-header .breadcrumb a,.page-header .breadcrumb a:link,.page-header .breadcrumb a:visited{color:#ede7f6}.page-header .breadcrumb a:active,.page-header .breadcrumb a:focus,.page-header .breadcrumb a:hover{color:#fff}.page-header .breadcrumb li:before{color:#ede7f6;text-shadow:0 1px 1px #6200ea}.page-header .page-tabs{background-color:transparent;margin-top:33.2px}.page-header .page-tabs li a{font-weight:700}.page-header .page-tabs li a,.page-header .page-tabs li a:link,.page-header .page-tabs li a:visited{background-color:transparent;border-radius:3px 3px 0 0;color:#ede7f6}.list-group,.threads-list .list-group{border-radius:3px}.page-header .page-tabs li a:focus,.page-header .page-tabs li a:hover{background-color:#7c4dff;color:#fff}@media screen and (max-width:991px){.page-header .page-tabs li a:focus,.page-header .page-tabs li a:hover{background-color:transparent;color:#ede7f6}}.page-header .page-tabs li.active a,.page-header .page-tabs li.active a:active,.page-header .page-tabs li.active a:focus,.page-header .page-tabs li.active a:hover,.page-header .page-tabs li.active a:link,.page-header .page-tabs li.active a:visited{background-color:#fff;color:#424242}.misago-footer{background:#fff;margin-top:40px}.misago-footer .footer-content{border-top:1px solid #e6e6e6;padding-top:20px;color:#bdbdbd}@media screen and (max-width:767px){.misago-footer .footer-content{text-align:center}.misago-footer .footer-content .site-footnote{clear:both;margin-bottom:15px}}.misago-footer .footer-content a,.misago-footer .footer-content a:link,.misago-footer .footer-content a:visited{color:#757575}.misago-footer .footer-content a:focus,.misago-footer .footer-content a:hover{color:#212121}.misago-footer .footer-content .misago-branding,.misago-footer .footer-content .misago-branding:link,.misago-footer .footer-content .misago-branding:visited{color:#b388ff}.misago-footer .footer-content .misago-branding:focus,.misago-footer .footer-content .misago-branding:hover{color:#8f67ff}.list-group{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.list-group .list-group-item{border-left-color:#ddd;border-right-color:#ddd}.list-group .list-group-item:first-child{border-top-color:#ddd}.list-group .list-group-item:last-child{border-bottom-color:#ddd}.list-group-item.empty-message{padding-top:20px;padding-bottom:20px;text-align:center}.list-group-item.empty-message p{margin:10px 0}.threads-list .thread-last-action img,.threads-list .thread-main img{border-radius:4px}.threads-list .thread-last-action .thread-last-reply,.threads-list .thread-last-action .thread-last-reply:link,.threads-list .thread-last-action .thread-last-reply:visited{color:#777;font-size:12px}.threads-list .thread-last-action .thread-last-reply:active,.threads-list .thread-last-action .thread-last-reply:focus,.threads-list .thread-last-action .thread-last-reply:hover{color:#555}.threads-list .thread-details-top{color:#777}.threads-list .thread-details-top a,.threads-list .thread-details-top a:link,.threads-list .thread-details-top a:visited{color:#777;font-size:12px}.threads-list .thread-details-top a:active,.threads-list .thread-details-top a:focus,.threads-list .thread-details-top a:hover{color:#212121}.threads-list .thread-details-top .thread-detail-new,.threads-list .thread-details-top .thread-detail-new:link,.threads-list .thread-details-top .thread-detail-new:visited{color:#a0f}.threads-list .thread-details-top .thread-detail-new:active,.threads-list .thread-details-top .thread-detail-new:focus,.threads-list .thread-details-top .thread-detail-new:hover{color:#80c}.threads-list .thread-details-top .thread-detail-pinned-globally{color:#3d5afe}.threads-list .thread-details-top .thread-detail-pinned-locally{color:#8c9eff}.threads-list .thread-details-top .thread-detail-unapproved{color:#ef6c00}.threads-list .thread-details-top a.thread-detail-answered{color:#388e3c}.threads-list .thread-details-top .thread-detail-unapproved-posts{color:#f4511e}.threads-list .thread-details-bottom{color:#777}.threads-list .thread-details-bottom a,.threads-list .thread-details-bottom a:link,.threads-list .thread-details-bottom a:visited{color:#555;font-size:12px}.threads-list .thread-details-bottom a:active,.threads-list .thread-details-bottom a:focus,.threads-list .thread-details-bottom a:hover{color:#212121}.threads-list .thread-read .thread-title,.threads-list .thread-read .thread-title:active,.threads-list .thread-read .thread-title:focus,.threads-list .thread-read .thread-title:hover,.threads-list .thread-read .thread-title:link,.threads-list .thread-read .thread-title:visited{color:#555}.threads-diff-message .btn,.threads-diff-message .btn.disabled,.threads-diff-message .btn.disabled:active,.threads-diff-message .btn.disabled:hover,.threads-diff-message .btn:disabled,.threads-diff-message .btn:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.threads-diff-message .btn:focus,.threads-diff-message .btn:hover{background:#6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .threads-diff-message .btn,.dropdown.open .threads-diff-message .btn:active:focus,.dropdown.open .threads-diff-message .btn:focus,.dropdown.open .threads-diff-message .btn:hover,.threads-diff-message .btn:active,.threads-diff-message .btn:active:focus{background:#4d00b7;border:1px solid #4d00b7;color:#fff;-webkit-box-shadow:none;box-shadow:none}.panel,.threads-diff-message .btn,.threads-diff-message .btn:active,.threads-diff-message .btn:focus,.threads-diff-message .btn:focus:active,.threads-diff-message .btn:hover{border:none}.threads-diff-message .btn.btn-loading,.threads-diff-message .btn.btn-loading:active,.threads-diff-message .btn.btn-loading:active:focus,.threads-diff-message .btn.btn-loading:disabled,.threads-diff-message .btn.btn-loading:disabled:hover,.threads-diff-message .btn.btn-loading:focus,.threads-diff-message .btn.btn-loading:hover{color:transparent}.nav-side{-webkit-box-shadow:none;box-shadow:none}.panel-form,.panel-participants,.panel-post{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.nav-side>.list-group-item{margin-bottom:1px}.nav-side>.list-group-item:first-child{border-top:none}.nav-side>.list-group-item:last-child{border-bottom:none}.nav-side>.list-group-item,.nav-side>.list-group-item:link,.nav-side>.list-group-item:visited{background:#f5f5f5;border:none;color:#757575}.nav-side>.list-group-item:active,.nav-side>.list-group-item:focus,.nav-side>.list-group-item:hover{background:#e0e0e0;color:#424242}.nav-side>.list-group-item.active,.nav-side>.list-group-item.active:active,.nav-side>.list-group-item.active:focus,.nav-side>.list-group-item.active:hover,.nav-side>.list-group-item.active:link,.nav-side>.list-group-item.active:visited{background:#651fff;color:#fff;font-weight:700}.nav-side>.list-group-item.active .badge,.nav-side>.list-group-item.active:active .badge,.nav-side>.list-group-item.active:focus .badge,.nav-side>.list-group-item.active:hover .badge,.nav-side>.list-group-item.active:link .badge,.nav-side>.list-group-item.active:visited .badge{background:#651fff;color:#fff}.panel,.username-history .user-avatar{border-radius:4px}.panel .panel-footer,.panel .panel-heading{background:#fff}.panel .panel-title{color:#777}.panel .form-group+.form-group{margin-top:20px}.panel .panel-body>.form-group:first-child{margin-top:10px}.panel fieldset{margin-top:20px}.panel fieldset:first-child{margin-top:0}.panel fieldset legend{border:none;color:#555;font-weight:300}.panel-message-body .message-icon{color:#9575cd}.panel-form{border:1px solid #ddd}.poll-form .panel{margin:0}.category-main .media-left .read-status{background:#eee;border-radius:4px;padding:5px 8px}.category-main .media-left .material-icon{height:14px;width:14px;color:#555;font-size:14px;line-height:14px}.category-main .media-left .read-status.item-new{background-color:#651fff}.category-main .media-left .read-status.item-new .material-icon{color:#fff}@media screen and (min-width:768px){.category-main .media-left{padding:5px 0}}.category-main .media-body{padding-left:12px}.list-group-category-no-description .category-main .media-heading{margin-top:10px}.category-main .media-heading a,.category-main .media-heading a:link,.category-main .media-heading a:visited{color:#333}.category-main .media-heading a:active,.category-main .media-heading a:focus,.category-main .media-heading a:hover{color:#212121}.category-stats{color:#555}.category-main .category-description p{font-size:12px}.category-main .category-description p:last-child{margin-bottom:0}.category-last-thread img{background-color:#fff;border-radius:4px}.category-last-thread .media-heading{margin-bottom:2px}@media screen and (max-width:767px){.category-main .media-body{padding-left:3.33px}.list-group-category-no-description .category-main .media-heading{margin-top:5px}.category-last-thread .media-heading{margin-top:10px}}.category-last-thread .list-inline{color:#777;font-size:12px}.category-last-thread .list-inline a,.category-last-thread .list-inline a:link,.category-last-thread .list-inline a:visited{color:#777}.category-last-thread .list-inline .item-title,.category-last-thread .list-inline .item-title:link,.category-last-thread .list-inline .item-title:visited,.category-last-thread .list-inline a:active,.category-last-thread .list-inline a:focus,.category-last-thread .list-inline a:hover{color:#333}.category-last-thread .list-inline .item-title:active,.category-last-thread .list-inline .item-title:focus,.category-last-thread .list-inline .item-title:hover{color:#212121}.category-thread-message{color:#777}.category-thread-message .material-icon{padding:6px 0;margin-right:3px}@media screen and (max-width:767px){.category-thread-message{padding-top:15px;font-size:12px}.category-thread-message .material-icon{padding:3px 0}.panel-participants p{margin-top:10px;text-align:center}}.panel-participants{border:1px solid #ddd}.participant-card .btn-user{border:0!important}.panel-poll,.panel-post{border:1px solid #ddd}.participant-card .dropdown-header-owner{color:#651fff}.participant-card .dropdown-header-owner .material-icon{width:14px;height:14px;font-size:14px;line-height:14px}.panel-poll .poll-chart,.panel-poll .poll-details{color:#777;font-size:12px}.participant-card .dropdown-header-owner .icon-text{margin-left:4px;position:relative;top:2px}.panel-participants p{color:#777}.panel-poll .poll-select-choices{margin-top:-10px}.panel-poll .poll-details{margin-bottom:20px}.panel-poll .progress{margin-top:6.67px;margin-bottom:5px}.panel-poll .poll-options,.posting-message .btn{margin-top:20px}.posting-message .material-icon{color:#9575cd}.panel-post{background:#fff}.post-side{color:#777}.post-side .poster-avatar{border-radius:4px}.post-side .user-title,.post-side .user-title a,.post-side .user-title a:active,.post-side .user-title a:focus,.post-side .user-title a:hover,.post-side .user-title a:link,.post-side .user-title a:visited{color:#555}.post-heading .label-unread{background-color:#a0f;margin-right:16px}.post-heading .label-protected{background-color:transparent;margin-left:24px;position:relative;top:1px;color:#bdbdbd}.post-heading .label-protected .material-icon{margin-right:2px;position:relative;top:-1px;font-size:16px;line-height:16px}.post-heading>.btn-link{padding-left:0;padding-right:0}.post-heading>.btn-link,.post-heading>.btn-link:link,.post-heading>.btn-link:visited{color:#777}.post-heading>.btn-link:active,.post-heading>.btn-link:focus,.post-heading>.btn-link:focus:active,.post-heading>.btn-link:hover{color:#212121;text-decoration:none}.post-heading .btn-see-edits{margin-left:24px}.post-status-message{border-radius:4px;margin-top:10px;padding:6px 12px}.post-body:last-child{padding-bottom:10px}.post-attachments{background-color:#f2f2f2;border:none;border-radius:4px;margin-bottom:30px}.post-attachments:last-child{margin-bottom:10px}@media screen and (max-width:767px){.post-status-message{font-size:12px}.post-status-message .material-icon{margin-top:3px}.post-status-message p{margin-top:0}.post-attachments{border-radius:0;margin:0 -15px 20px}}.misago-markup img,.user-card-avatar img,.user-card-small-avatar img{border-radius:4px}.post-footer>.btn-link{padding-left:0;padding-right:0}.post-footer>.btn-link,.post-footer>.btn-link:link,.post-footer>.btn-link:visited{color:#777}.post-footer>.btn-link:active,.post-footer>.btn-link:focus,.post-footer>.btn-link:focus:active,.post-footer>.btn-link:hover{color:#212121;text-decoration:none}.post-footer p{color:#777;font-size:12px}.post-body-hidden,.post-body-invalid{padding-top:10px;padding-bottom:10px}.post-body-hidden .lead,.post-body-invalid .lead{margin-bottom:10px}.post-body-hidden .text-muted,.post-body-invalid .text-muted{margin-bottom:0;font-size:12px}.post-hidden{opacity:.75;filter:alpha(opacity=75)}.post-feed .panel-body{padding-bottom:0}.post-feed .post-body{position:relative;padding-top:0;padding-bottom:20px;max-height:300px;overflow-y:hidden}.post-feed .post-body:after{box-shadow:0 0 16px 16px #fff;display:block;position:absolute;bottom:0;height:0;width:100%;content:'-';color:transparent}.posts-list .event .event-label{margin-bottom:5px}.posts-list .event .label-unread{background-color:#a0f;color:#fff}.posts-list .event-info .btn-link,.posts-list .event-info a,.posts-list .event-info a:link,.posts-list .event-info a:visited{color:#555}.posts-list .event-info .btn-link:active,.posts-list .event-info .btn-link:focus,.posts-list .event-info .btn-link:focus:active,.posts-list .event-info a:active,.posts-list .event-info a:focus,.posts-list .event-info a:focus:active{color:#212121}.posts-list .event-hidden{opacity:.33;filter:alpha(opacity=33)}.user-card{background:#f5f5f5}.user-card-avatar{margin:20px 0}.user-card-avatar img{width:150px;height:150px}.user-card-username a,.user-card-username a:active,.user-card-username a:focus,.user-card-username a:hover,.user-card-username a:link,.user-card-username a:visited{color:#212121;font-size:18px;font-weight:700}.user-card-title a,.user-card-title a:link,.user-card-title a:visited,.user-card-title span{color:#555}.user-card-title a:active,.user-card-title a:focus,.user-card-title a:hover{color:#212121}@media screen and (min-width:768px){.user-card-stats{margin-top:20px}}.user-card-stats ul{margin:0}.user-card-stats li{display:inline-block;margin-right:12px;color:#777;font-size:12px}.user-card-stats li.user-stat-empty{display:none}@media screen and (min-width:768px){.user-card-stats li{margin:0 6px}li.user-stat-divider{display:block;margin:0}.user-card-stats{min-height:60px}}@media screen and (max-width:767px){li.user-stat-divider{display:none}.user-card-left{padding-right:0}}.progress,.progress .progress-bar{-webkit-box-shadow:none;box-shadow:none;height:8px}.page-user-profile .page-header img,.well{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.misago-markup .quote-block,.misago-markup blockquote{background-color:#ffecb3;border-color:#b3e5fc;overflow:hidden;color:#3e2723}.misago-markup .quote-block .quote-heading,.misago-markup blockquote .quote-heading{background-color:#ffe082;border:none;color:#795548}.misago-markup .quote-block .quote-heading a,.misago-markup .quote-block .quote-heading a:link,.misago-markup .quote-block .quote-heading a:visited,.misago-markup blockquote .quote-heading a,.misago-markup blockquote .quote-heading a:link,.misago-markup blockquote .quote-heading a:visited{color:#5d4037}.misago-markup .quote-block .quote-heading a:active,.misago-markup .quote-block .quote-heading a:focus,.misago-markup .quote-block .quote-heading a:hover,.misago-markup blockquote .quote-heading a:active,.misago-markup blockquote .quote-heading a:focus,.misago-markup blockquote .quote-heading a:hover{color:#3e2723}.misago-markup .quote-block hr,.misago-markup blockquote hr{border-color:#ffca28}.misago-markup>.quote-block,.misago-markup>blockquote{background:#ffecb3;border:none;border-radius:4px}.misago-markup .quote-body,.misago-markup>.quote-block .quote-block,.misago-markup>blockquote .quote-block{background:#ffecb3}.misago-markup .quote-body>.quote-block,.misago-markup .quote-body>blockquote{border-color:#ffe082;border-radius:4px}.modal-header{background:#eee;border-bottom-color:#eee;border-radius:6px 6px 0 0;color:#424242}.modal-header .close{padding:0 6px;color:#424242;font-size:24px;line-height:24px;text-shadow:none}.modal-message .message-icon{color:#9575cd}.modal-body>.form-group{margin:20px 0}.modal-body>.form-group:first-child{margin-top:0}.modal-body>.form-group:last-child{margin-bottom:0}.modal-sign-in .modal-body{padding-top:0;padding-bottom:0}.modal-sign-in .modal-body>.form-group{margin:20px 0}.legal-footnote{text-align:center}.legal-footnote .material-icon{margin-right:4px;position:relative;bottom:1px;color:#777;font-size:28px;line-height:28px}.legal-footnote a,.legal-footnote a:link,.legal-footnote a:visited{color:#777}.legal-footnote a:active,.legal-footnote a:focus,.legal-footnote a:hover{color:#212121}.modal-avatar-index .avatar-preview{background:#fff;border-radius:6px}.modal-avatar-crop .cropit-preview{background:#fff;margin:20px 0}.modal-avatar-crop .cropit-image-zoom-input{margin-top:40px;margin-bottom:20px}.modal-post-likers .media{border-bottom:1px solid #eee;padding-bottom:15px}.modal-post-likers .media img{border-radius:4px;width:40px;height:40px}.modal-post-likers .media:last-child{border:none;padding-bottom:0}.well{border:1px solid #ddd}.active-posters .rank-user-avatar img{border-radius:4px}.active-posters .user-details{color:#777}.active-posters .user-details a.rank-name,.active-posters .user-details a.rank-name:link,.active-posters .user-details a.rank-name:visited{color:#555;font-weight:400}.active-posters .user-details a.rank-name:active,.active-posters .user-details a.rank-name:focus,.active-posters .user-details a.rank-name:hover{color:#212121}.active-posters .user-details span.rank-name{color:#555;font-weight:400}.active-posters small{color:#777}.page-error .message-icon{color:#ef5350}.page-message .message-icon,.page-message-info .message-icon{color:#9575cd}.page-message-success .message-icon{color:#81c784}.page-options .message-line,.page-options .message-line a,.page-options .message-line a:link,.page-options .message-line a:visited{color:#777}.page-options .message-line a:active,.page-options .message-line a:focus,.page-options .message-line a:focus:active,.page-options .message-line a:hover{color:#212121}.page-user-profile .page-header img{background-color:#fff;border:3px solid #fff;border-radius:6px}.page-user-profile .page-header .user-status .status-icon{width:14px;height:14px;font-size:14px;line-height:14px}.page-user-profile .profile-side-avatar img{background-color:#fff;border:3px solid #fff;border-radius:6px;box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.panel-profile-details-group.panel{border:1px solid #ddd}.panel-profile-details-group.panel .form-group{margin:0}.panel-profile-details-group .form-control-static p:last-child{margin-bottom:0}.list-group-item.list-group-category-has-flavor{border-left:5px solid #fff;padding-left:10px}.list-group-item.list-group-item-category-red{border-left-color:#f44336}.list-group-item.list-group-item-category-light-red{border-left-color:#ef9a9a}.list-group-item.list-group-item-category-pink{border-left-color:#e91e63}.list-group-item.list-group-item-category-light-pink{border-left-color:#f48fb1}.list-group-item.list-group-item-category-purple{border-left-color:#9c27b0}.list-group-item.list-group-item-category-light-purple{border-left-color:#ce93d8}.list-group-item.list-group-item-category-deep-purple{border-left-color:#673ab7}.list-group-item.list-group-item-category-indigo{border-left-color:#3f51b5}.list-group-item.list-group-item-category-light-indigo{border-left-color:#9fa8da}.list-group-item.list-group-item-category-blue{border-left-color:#2196f3}.list-group-item.list-group-item-category-light-blue{border-left-color:#81d4fa}.list-group-item.list-group-item-category-cyan{border-left-color:#00bcd4}.list-group-item.list-group-item-category-light-cyan{border-left-color:#80deea}.list-group-item.list-group-item-category-teal{border-left-color:#009688}.list-group-item.list-group-item-category-light-teal{border-left-color:#80cbc4}.list-group-item.list-group-item-category-green{border-left-color:#4caf50}.list-group-item.list-group-item-category-light-green{border-left-color:#aed581}.list-group-item.list-group-item-category-lime{border-left-color:#cddc39}.list-group-item.list-group-item-category-yellow{border-left-color:#ffeb3b}.list-group-item.list-group-item-category-amber{border-left-color:#ffc107}.list-group-item.list-group-item-category-orange{border-left-color:#ff9800}.list-group-item.list-group-item-category-deep-orange{border-left-color:#ff5722}.list-group-item.list-group-item-category-brown{border-left-color:#795548}.list-group-item.list-group-item-category-light-brown{border-left-color:#bcaaa4}.list-group-item.list-group-item-category-blue-grey{border-left-color:#607d8b}.list-group-item.list-group-item-category-light-blue-grey{border-left-color:#b0bec5}.list-group-item.list-group-item-category-grey{border-left-color:#9e9e9e}.list-group-item.list-group-item-category-black{border-left-color:#000}.post-primary .panel-post{border-color:#b388ff}.post-primary .user-title,.post-primary .user-title a,.post-primary .user-title a:active,.post-primary .user-title a:focus,.post-primary .user-title a:hover,.post-primary .user-title a:link,.post-primary .user-title a:visited{color:#6200ea}.post-success .panel-post{border-color:#00c853}.post-success .user-title,.post-success .user-title a,.post-success .user-title a:active,.post-success .user-title a:focus,.post-success .user-title a:hover,.post-success .user-title a:link,.post-success .user-title a:visited{color:#388e3c}.post-warning .panel-post{border-color:#ffab40}.post-warning .user-title,.post-warning .user-title a,.post-warning .user-title a:active,.post-warning .user-title a:focus,.post-warning .user-title a:hover,.post-warning .user-title a:link,.post-warning .user-title a:visited{color:#ff6d00}.post-danger .panel-post{border-color:#ff8a80}.post-danger .user-title,.post-danger .user-title a,.post-danger .user-title a:active,.post-danger .user-title a:focus,.post-danger .user-title a:hover,.post-danger .user-title a:link,.post-danger .user-title a:visited{color:#d50000}.user-card-primary .panel-body{background:#fff;border:2px solid #b388ff;border-radius:4px;padding:13px}.user-card-primary .user-card-title,.user-card-primary .user-card-title a.user-title,.user-card-primary .user-card-title a.user-title:active,.user-card-primary .user-card-title a.user-title:focus,.user-card-primary .user-card-title a.user-title:hover,.user-card-primary .user-card-title a.user-title:link,.user-card-primary .user-card-title a.user-title:visited{color:#6200ea}.user-card-primary .user-card-stats li{color:#9575cd}.user-card-success .panel-body{background:#fff;border:2px solid #00c853;border-radius:4px;padding:13px}.user-card-success .user-card-title,.user-card-success .user-card-title a.user-title,.user-card-success .user-card-title a.user-title:active,.user-card-success .user-card-title a.user-title:focus,.user-card-success .user-card-title a.user-title:hover,.user-card-success .user-card-title a.user-title:link,.user-card-success .user-card-title a.user-title:visited{color:#388e3c}.user-card-success .user-card-stats li{color:#66bb6a}.user-card-warning .panel-body{background:#fff;border:2px solid #ffab40;border-radius:4px;padding:13px}.user-card-warning .user-card-title,.user-card-warning .user-card-title a.user-title,.user-card-warning .user-card-title a.user-title:active,.user-card-warning .user-card-title a.user-title:focus,.user-card-warning .user-card-title a.user-title:hover,.user-card-warning .user-card-title a.user-title:link,.user-card-warning .user-card-title a.user-title:visited{color:#ff6d00}.user-card-warning .user-card-stats li{color:#ff6e40}.user-card-danger .panel-body{background:#fff;border:2px solid #ff8a80;border-radius:4px;padding:13px}.user-card-danger .user-card-title,.user-card-danger .user-card-title a.user-title,.user-card-danger .user-card-title a.user-title:active,.user-card-danger .user-card-title a.user-title:focus,.user-card-danger .user-card-title a.user-title:hover,.user-card-danger .user-card-title a.user-title:link,.user-card-danger .user-card-title a.user-title:visited{color:#d50000}.user-card-danger .user-card-stats li{color:#e57373}.list-group .list-group-rank-primary{border-left:4px solid #7e57c2;padding-left:11px}.list-group .list-group-rank-primary a.rank-name,.list-group .list-group-rank-primary a.rank-name:active,.list-group .list-group-rank-primary a.rank-name:focus,.list-group .list-group-rank-primary a.rank-name:hover,.list-group .list-group-rank-primary a.rank-name:link,.list-group .list-group-rank-primary a.rank-name:visited,.list-group .list-group-rank-primary span.rank-name{color:#6200ea}.list-group .list-group-rank-success{border-left:4px solid #9ccc65;padding-left:11px}.list-group .list-group-rank-success a.rank-name,.list-group .list-group-rank-success a.rank-name:active,.list-group .list-group-rank-success a.rank-name:focus,.list-group .list-group-rank-success a.rank-name:hover,.list-group .list-group-rank-success a.rank-name:link,.list-group .list-group-rank-success a.rank-name:visited,.list-group .list-group-rank-success span.rank-name{color:#388e3c}.list-group .list-group-rank-warning{border-left:4px solid #ff7043;padding-left:11px}.list-group .list-group-rank-warning a.rank-name,.list-group .list-group-rank-warning a.rank-name:active,.list-group .list-group-rank-warning a.rank-name:focus,.list-group .list-group-rank-warning a.rank-name:hover,.list-group .list-group-rank-warning a.rank-name:link,.list-group .list-group-rank-warning a.rank-name:visited,.list-group .list-group-rank-warning span.rank-name{color:#ff6d00}.list-group .list-group-rank-danger{border-left:4px solid #f44336;padding-left:11px}.list-group .list-group-rank-danger a.rank-name,.list-group .list-group-rank-danger a.rank-name:active,.list-group .list-group-rank-danger a.rank-name:focus,.list-group .list-group-rank-danger a.rank-name:hover,.list-group .list-group-rank-danger a.rank-name:link,.list-group .list-group-rank-danger a.rank-name:visited,.list-group .list-group-rank-danger span.rank-name{color:#d50000}.page-header-rank-primary .user-rank,.page-header-rank-primary .user-rank a,.page-header-rank-primary .user-rank a:active,.page-header-rank-primary .user-rank a:focus,.page-header-rank-primary .user-rank a:hover,.page-header-rank-primary .user-rank a:link,.page-header-rank-primary .user-rank a:visited{color:#6200ea}.page-header-rank-success .user-rank,.page-header-rank-success .user-rank a,.page-header-rank-success .user-rank a:active,.page-header-rank-success .user-rank a:focus,.page-header-rank-success .user-rank a:hover,.page-header-rank-success .user-rank a:link,.page-header-rank-success .user-rank a:visited{color:#388e3c}.page-header-rank-warning .user-rank,.page-header-rank-warning .user-rank a,.page-header-rank-warning .user-rank a:active,.page-header-rank-warning .user-rank a:focus,.page-header-rank-warning .user-rank a:hover,.page-header-rank-warning .user-rank a:link,.page-header-rank-warning .user-rank a:visited{color:#ff6d00}.page-header-rank-danger .user-rank,.page-header-rank-danger .user-rank a,.page-header-rank-danger .user-rank a:active,.page-header-rank-danger .user-rank a:focus,.page-header-rank-danger .user-rank a:hover,.page-header-rank-danger .user-rank a:link,.page-header-rank-danger .user-rank a:visited{color:#d50000}
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.btn,.caret,img{vertical-align:middle}hr,img{border:0}body,figure{margin:0}.img-thumbnail,.table,label{max-width:100%}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}.alerts-snackbar,.form-control-feedback,a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}.img-thumbnail,body{background-color:#fff}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#212121}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#651fff;text-decoration:none}a:focus,a:hover{color:#4100d2;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}dl,ol,ul{margin-top:0}.lead,address,dl{margin-bottom:20px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{font-size:16px;font-weight:300;line-height:1.4}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#651fff}a.text-primary:focus,a.text-primary:hover{color:#4900eb}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#651fff}a.bg-primary:focus,a.bg-primary:hover{background-color:#4900eb}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:700px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{font-style:normal}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.badge,.btn,.dropdown-header,.input-group-btn,.label,.material-icon{white-space:nowrap}.pre-scrollable{overflow-y:scroll}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#616161}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.btn-block,input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;touch-action:manipulation;cursor:pointer;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle,.btn.active,.btn:active,.dropdown-toggle:focus,.navbar-toggle:focus,.open>a{outline:0}.btn.focus,.btn:focus,.btn:hover{color:#757575;text-decoration:none}.btn.active,.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{background-color:#f5f5f5}.btn-default.focus,.btn-default:focus{color:#757575;background-color:#dcdcdc;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#757575;background-color:#dcdcdc;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#757575;background-color:#cacaca;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#f5f5f5;border-color:#ccc}.btn-default .badge{color:#f5f5f5;background-color:#757575}.btn-primary{background-color:#a36eff}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#823bff;border-color:#4d00d4}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#823bff;border-color:#6b18ff}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#6b18ff;border-color:#4d00d4}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#a36eff;border-color:#9255ff}.btn-primary .badge{color:#a36eff;background-color:#fff}.btn-success{background-color:#00c853}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#00953e;border-color:#002f14}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#00953e;border-color:#00712f}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#00712f;border-color:#002f14}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#00c853;border-color:#00af48}.btn-success .badge{color:#00c853;background-color:#fff}.btn-info{color:#fff;background-color:#3d5afe;border-color:#2444fe}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#0a2ffe;border-color:#0119a1}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#0a2ffe;border-color:#0123e3}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#0123e3;border-color:#0119a1}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#3d5afe;border-color:#2444fe}.btn-info .badge{color:#3d5afe;background-color:#fff}.btn-warning{color:#fff;background-color:#ef6c00;border-color:#d66000}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#bc5500;border-color:#562700}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#bc5500;border-color:#984500}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#984500;border-color:#562700}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#ef6c00;border-color:#d66000}.btn-warning .badge{color:#ef6c00;background-color:#fff}.btn-danger{background-color:#ef5350}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#eb2521;border-color:#98110e}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#eb2521;border-color:#d51713}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#d51713;border-color:#98110e}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#ef5350;border-color:#ed3c39}.btn-danger .badge{color:#ef5350;background-color:#fff}.btn-link{color:#651fff;font-weight:400;border-radius:0}.alert .alert-link,.label{font-weight:700}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#4100d2;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block}.btn-block+.btn-block{margin-top:5px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.nav>li,.nav>li>a,.open>.dropdown-menu{display:block}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#424242}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#651fff}.dropdown-header,.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.nav>li.disabled>a{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.nav>li,.nav>li>a,.navbar{position:relative}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:700px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#651fff}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#651fff}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{min-height:54px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:700px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:17px 15px;font-size:18px;line-height:20px;height:54px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:700px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:10px;margin-bottom:10px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:8.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:699px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:700px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:17px;padding-bottom:17px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:10px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:699px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:700px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-text{float:left;margin-left:15px;margin-right:15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-sm{margin-top:12px;margin-bottom:12px}.navbar-btn.btn-xs{margin-top:16px;margin-bottom:16px}.navbar-text{margin-top:17px;margin-bottom:17px}.alert,.breadcrumb{margin-bottom:20px}@media (min-width:700px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.progress-bar{float:left}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:699px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#e0e0e0}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#e7e7e7}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#fff}@media (max-width:699px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#e0e0e0}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#e7e7e7}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#e0e0e0}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#e0e0e0}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#ccc}.breadcrumb{padding:8px 15px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#43a047;border-color:#43a047;color:#fff}.alert-success hr{border-top-color:#3b8e3f}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#448aff;border-color:#448aff;color:#fff}.alert-info hr{border-top-color:#2a7aff}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#ef6c00;border-color:#ef6c00;color:#fff}.alert-warning hr{border-top-color:#d66000}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#f44336;border-color:#f44336;color:#fff}.alert-danger hr{border-top-color:#f32c1e}.alert-danger .alert-link{color:#e6e6e6}.label{display:inline;padding:.2em .6em .3em;font-size:75%;line-height:1;color:#fff;text-align:center;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#651fff}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#4900eb}.label-success{background-color:#43a047}.label-success[href]:focus,.label-success[href]:hover{background-color:#347c37}.label-info{background-color:#3d5afe}.label-info[href]:focus,.label-info[href]:hover{background-color:#0a2ffe}.label-warning{background-color:#ef6c00}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#bc5500}.label-danger{background-color:#f44336}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#ea1c0d}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{margin-bottom:20px;background-color:#eee;border-radius:3px}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.progress-bar{width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#651fff;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#00c853}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#3d5afe}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#ff9100}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#ff1744}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.badge,.material-icon{vertical-align:middle}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #eee}.list-group-item:first-child,.panel-heading{border-top-right-radius:3px;border-top-left-radius:3px}.list-group-item:last-child,.panel-footer{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.list-group-item:last-child{margin-bottom:0}a.list-group-item,button.list-group-item{color:#777}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#212121;background-color:#fff}button.list-group-item{width:100%;text-align:left}.badge,.pager{text-align:center}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#651fff;background-color:#fff;border-color:#eee}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#fff}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel,.panel-footer{background-color:#fff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:14px 15px;border-bottom:1px solid transparent}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:14px 15px;border-top:1px solid #ddd}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#fff;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#fff;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#651fff}.panel-primary>.panel-heading{color:#fff;background-color:#651fff;border-color:#651fff}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#651fff}.panel-primary>.panel-heading .badge{color:#651fff;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#651fff}.panel-success{border-color:#00c853}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#00c853}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#00c853}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#00c853}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#f44336}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#f44336}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f44336}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f44336}.pager{padding-left:0;margin:20px 0;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#651fff;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.close,.list-group-item>.badge{float:right}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#4100d2;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#651fff;border-color:#651fff;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.close{font-weight:700;line-height:1}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fff;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{font-size:21px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.agreement-content,.modal-content{background-clip:padding-box;outline:0}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;background-color:#777;border-radius:10px}.badge:empty,.modal{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#651fff;background-color:#fff}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.affix,.auth-message{position:fixed}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.agreement-footer:after,.agreement-footer:before,.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.agreement-footer:after,.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.header-stats,.page-header .breadcrumb,.page-header .go-back-sm,.page-header h1{text-shadow:0 1px 1px #6200ea}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}@media screen and (min-width:992px){.md-margin-top-no{margin-top:0!important}}@media screen and (min-width:768px) and (max-width:991px){.sm-margin-top{margin-top:20px!important}.sm-margin-top-no{margin-top:0!important}.sm-margin-top-half{margin-top:6.6px!important}.sm-align-row-buttons{margin-top:5px!important}}@media screen and (max-width:767px){.xs-margin-top{margin-top:20px!important}.xs-margin-top-half{margin-top:6.6px!important}}.auth-message{background-color:#eee;width:100%;top:-100%;left:0;z-index:1070;transition:top .3s ease}.agreement-overlay,.alerts-snackbar{position:fixed;z-index:1060;width:100%}.auth-message.show{top:0;bottom:auto}.auth-message p{padding:5px 0}@media screen and (max-width:991px){body,html{overflow-x:hidden}.auth-message{text-align:center}.auth-message .btn{padding:10px 16px;font-size:18px}}.alerts-snackbar{top:-100%;text-align:center;font-size:18px;transition:top .3s ease}.alerts-snackbar.in{top:0;transition:top .2s ease}.alerts-snackbar p{display:inline-block;border-radius:0 0 4px 4px;margin:0;pointer-events:all}.agreement-overlay{height:100%;top:0;left:0;background-color:rgba(0,0,0,.5);overflow-x:scroll}.agreement-overlay .container{max-width:900px;padding:20px 30px}.agreement-content{background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);padding:10px 16px}.agreement-header{padding:10px 0;border-bottom:1px solid #eee}.agreement-header h2{margin-top:0}.agreement-header p{margin:0;padding:0}.agreement-body{padding:20px 0}.agreement-body p.lead{margin:0;padding:0}.agreement-footer{text-align:right;border-top:1px solid #e5e5e5;padding:10px 0}.agreement-footer .btn+.btn{margin-left:5px;margin-bottom:0}.agreement-footer .btn-group .btn+.btn{margin-left:-1px}.agreement-footer .btn-block+.btn-block{margin-left:0}body.agreement-overlay-visible{overflow:hidden}.loader{width:100%;height:49px;text-align:center}.loader.loader-spaced{margin:40px 0}.loader-spinning-wheel{width:49px;height:49px;margin:0 auto;border:3px solid #777;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:cssload-spin 575ms infinite linear;-o-animation:cssload-spin 575ms infinite linear;-ms-animation:cssload-spin 575ms infinite linear;-webkit-animation:cssload-spin 575ms infinite linear;-moz-animation:cssload-spin 575ms infinite linear}@keyframes cssload-spin{100%{transform:rotate(360deg)}}@-o-keyframes cssload-spin{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes cssload-spin{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes cssload-spin{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes cssload-spin{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}.navbar{margin-bottom:0}.navbar .navbar-full .navbar-brand{padding-top:11px;padding-bottom:11px;font-size:21px}.navbar .navbar-full .navbar-brand>*{display:inline-block;vertical-align:middle}.navbar .navbar-full .navbar-brand img{height:32px;margin-right:6px}.navbar .navbar-full .navbar-nav>li>a{font-size:16.8px}.navbar .navbar-full .navbar-icon{display:block;height:54px;padding:13px 15px;position:relative;color:#e0e0e0}.navbar .navbar-full .navbar-icon:focus,.navbar .navbar-full .navbar-icon:hover{color:#fff;background-color:transparent}.navbar .navbar-full .navbar-icon .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.navbar .navbar-full .navbar-icon .badge{background-color:#f44336;position:absolute;top:6px;right:6px;font-size:9px}.nav-side>a>.material-icon,.navbar ul.navbar-compact-nav>li>a>.material-icon{font-size:24px;line-height:24px}.navbar .navbar-full .open .navbar-icon,.navbar .navbar-full .open .navbar-icon:focus,.navbar .navbar-full .open .navbar-icon:hover{background-color:#e7e7e7;color:#555}.navbar .navbar-full .nav-guest,.navbar .navbar-full .nav-user{float:right}.navbar .navbar-full .nav-guest .navbar-btn,.navbar .navbar-full .nav-user .navbar-btn{margin-left:15px}.navbar .navbar-full .nav-user .dropdown-toggle{padding:10px}.navbar .navbar-full .nav-user .dropdown-toggle img{width:34px;height:34px}.navbar ul.navbar-compact-nav{border-collapse:collapse;display:table;margin:0;table-layout:fixed;width:100%}.navbar ul.navbar-compact-nav>li{display:table-cell;width:100%}.navbar ul.navbar-compact-nav>li>a,.navbar ul.navbar-compact-nav>li>button{background:0 0;border:none;display:block;padding-top:13px;padding-bottom:13px;width:100%;color:#e0e0e0;text-align:center}.navbar ul.navbar-compact-nav>li>a.active,.navbar ul.navbar-compact-nav>li>a:focus,.navbar ul.navbar-compact-nav>li>a:hover,.navbar ul.navbar-compact-nav>li>button.active,.navbar ul.navbar-compact-nav>li>button:focus,.navbar ul.navbar-compact-nav>li>button:hover{color:#212121;background-color:#fafafa}.navbar ul.navbar-compact-nav>li>a>img,.navbar ul.navbar-compact-nav>li>button>img{width:24px;height:24px}.navbar ul.navbar-compact-nav>li>button{display:inline-block}@media (max-width:700px){.navbar.navbar-misago{min-height:auto}}.modal-body .form-group,.toolbar{min-height:34px}.navbar-misago .navbar-desktop-nav{display:none}@media (min-width:700px){.navbar-misago ul.navbar-compact-nav{display:none}.navbar-misago .navbar-desktop-nav{display:block}}.nav-side>a>.material-icon{margin:-5px 10px -5px -5px;position:relative;bottom:1px;width:24px;height:24px}.nav-side>a .badge{position:relative;top:1px}@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(../fonts/MaterialIcons-Regular.eot);src:local('Material Icons'),local('MaterialIcons-Regular'),url(../fonts/MaterialIcons-Regular.woff2) format('woff2'),url(../fonts/MaterialIcons-Regular.woff) format('woff'),url(../fonts/MaterialIcons-Regular.ttf) format('truetype')}.material-icon{font-family:'Material Icons';font-weight:400;font-style:normal;display:inline-block;width:1em;height:1em;line-height:1;text-align:center;text-transform:none;letter-spacing:normal;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:'liga'}.misago-markup h1,.misago-markup h2,.misago-markup h3,.misago-markup h4,.misago-markup h5,.misago-markup h6,.misago-markup p,.page-header h1,.post-attachments .attachment-name,.post-feed .post-heading .btn{word-wrap:break-word}.modal-toolbar{background:#eee;border-bottom:1px solid #e5e5e5;overflow:auto;padding:6px 12px}.modal-toolbar .pull-left{margin-right:8px}.modal-toolbar p{padding:5px 0;margin-bottom:0}@media screen and (max-width:991px){.modal-message{text-align:center}.modal-message .message-icon{margin:30px}.modal-message .message-icon .material-icon{font-size:160px}}@media screen and (min-width:992px){.modal-message .modal-body{padding-top:20px;padding-bottom:30px}.modal-message .message-icon{float:left;position:relative;left:7px}.modal-message .message-icon .material-icon{font-size:50px}.modal-message .message-body{margin-left:75px;margin-top:10px}.modal-message .message-body p{margin-top:20px}.modal-message .message-body .lead{margin-top:0;margin-bottom:0}}.modal-loader{padding:50px 0}.modal-loader .loader{width:100%;height:80px;text-align:center}.modal-loader .loader-spinning-wheel{width:80px;height:80px}.list-item-errors{margin-bottom:20px}.list-errored-items li:last-child .list-item-errors{margin-bottom:0}.modal-post-likers .media-list{margin:0}.modal-post-likers .item-title{display:block}.has-feedback .material-icon.form-control-feedback{top:6px;right:24px;font-size:1.42857143;line-height:1.42857143}.well.well-form.well-done{font-size:18px;text-align:center}.well.well-form.well-done .message-icon{margin-bottom:10px;font-size:90px;line-height:90px}.well.well-form.well-done .message-body{margin-bottom:20px}.well.well-form.well-noscript{font-size:18px;text-align:center}.well.well-form.well-noscript .message-icon{margin-bottom:10px;font-size:90px;line-height:90px}.btn.btn-select,.btn.btn-yes-no{background:0 0;border:1px solid #eee}.btn.btn-select .material-icon,.btn.btn-yes-no .material-icon{margin:-4px 8px -4px 0;position:relative;bottom:1px;width:20px;height:20px;font-size:20px;line-height:20px}@media screen and (max-width:767px){.btn.btn-yes-no{width:100%;overflow:auto}.btn.btn-yes-no .material-icon{float:left;margin-top:1px}.btn.btn-yes-no .btn-text{display:block;margin-left:30px;text-align:left;white-space:normal}}input.hidden-file-upload{position:absolute;top:-9999px;left:-9999px}.form-search{position:relative}.form-search .form-control{padding-right:30px}.form-search .material-icon{position:absolute;top:5px;right:5px;color:#777;font-size:24px;line-height:24px;pointer-events:none}.btn.btn-loading,.btn.btn-loading:active,.btn.btn-loading:focus,.btn.btn-loading:hover,.btn.btn-loading:link,.btn.btn-loading:visited{color:transparent}.btn.btn-loading .loader,.btn.btn-loading:active .loader,.btn.btn-loading:focus .loader,.btn.btn-loading:hover .loader,.btn.btn-loading:link .loader,.btn.btn-loading:visited .loader{height:20px;margin-top:-20px}.btn.btn-loading .loader>div,.btn.btn-loading:active .loader>div,.btn.btn-loading:focus .loader>div,.btn.btn-loading:hover .loader>div,.btn.btn-loading:link .loader>div,.btn.btn-loading:visited .loader>div{width:20px;height:20px}.btn.btn-loading.btn-default .loader>div{border-top-color:#757575;border-bottom-color:#757575}.btn.btn-loading.btn-danger .loader>div,.btn.btn-loading.btn-info .loader>div,.btn.btn-loading.btn-primary .loader>div,.btn.btn-loading.btn-success .loader>div,.btn.btn-loading.btn-warning .loader>div{border-top-color:#fff;border-bottom-color:#fff}.btn .material-icon{margin-right:3px;position:relative;bottom:1px}.btn-icon .material-icon{margin:-1px -4px;width:20px;height:20px;font-size:20px;line-height:20px}.btn-icon .btn-text{margin-left:10px}.btn-icon .btn-text-left{margin-right:10px}.btn-block.btn-icon{padding-left:0;padding-right:0}.dropdown-menu>li>.btn-link,.dropdown-menu>li>a,.modal-menu>li>.btn-link,.modal-menu>li>a{display:block;border:none;clear:both;float:none;padding:6px 20px;width:100%;color:#333;font-weight:400;line-height:1.42857143;text-align:left;white-space:nowrap}.pager-more,.user-dropdown .guest-preview,li.dropdown-search-message{text-align:center}.dropdown-menu>li>.btn-link:active,.dropdown-menu>li>.btn-link:focus,.dropdown-menu>li>.btn-link:hover,.dropdown-menu>li>a:active,.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover,.modal-menu>li>.btn-link:active,.modal-menu>li>.btn-link:focus,.modal-menu>li>.btn-link:hover,.modal-menu>li>a:active,.modal-menu>li>a:focus,.modal-menu>li>a:hover{background-color:#424242;color:#FAFAFA;text-decoration:none}.dropdown-menu>li>.btn-link .material-icon,.dropdown-menu>li>a .material-icon,.modal-menu>li>.btn-link .material-icon,.modal-menu>li>a .material-icon{margin:-2px 7px -2px 0;position:relative;bottom:1px;font-size:18px}.dropdown-menu>li>.btn-link .badge,.dropdown-menu>li>a .badge,.modal-menu>li>.btn-link .badge,.modal-menu>li>a .badge{float:right;position:relative;top:1px}.modal-menu{margin:20px 0;padding:0}.modal-menu>li{margin:6.67px 0;padding:0;list-style:none}.dropdown-menu{width:210px}.dropdown-menu .dropdown-footer{padding:6px 20px}.dropdown-menu .dropdown-buttons{padding:2px 20px 7px}.dropdown-menu .dropdown-buttons .btn{margin:4px 0}.mobile-dropdown{position:relative}.compact-nav.open>.dropdown-menu,.mobile-dropdown.open>.dropdown-menu{border:none;border-radius:0;display:block;margin:0;width:100%}.user-dropdown .guest-preview .row{margin:0}.navbar .user-dropdown{width:240px}.user-dropdown .dropdown-header{padding:6px 20px;font-size:18px}@media screen and (min-width:992px){.category-picker .dropdown-menu{width:300px}}.category-picker .dropdown-menu>li>.btn-link{white-space:normal;word-wrap:break-word}.dropdown-search-thread h5,.editor-attachment-complete .editor-attachment-details abbr,.page-tabs ul,.page-tabs ul a,.page-tabs ul li,.post-attachments abbr{white-space:nowrap}@media screen and (max-width:767px){.dropdown-menu.stick-to-bottom{border-radius:0;border:none;max-height:300px;overflow-y:auto;-webkit-box-shadow:0 0 30px #777;box-shadow:0 0 30px #777;clear:both;top:auto;width:100%;position:fixed;bottom:0;margin:0;padding:0 0 20px}.dropdown-menu.stick-to-bottom li{float:none;margin:0;clear:both}.dropdown-menu.stick-to-bottom li>.btn,.dropdown-menu.stick-to-bottom li>a{padding-top:15px;padding-bottom:15px;border-bottom:1px solid #e5e5e5}}.navbar-misago .dropdown-menu.dropdown-search-results{margin:0;padding-top:0;width:400px;left:auto;right:0}.dropdown-search-results .form-group{margin:0;padding:12px}.dropdown-search-thread{width:100%}.dropdown-search-thread h5{margin:0;overflow:hidden;text-overflow:ellipsis}.dropdown-search-thread small{display:block;margin:4px 0 0}.page-header-bg{margin-bottom:20px}.page-header{margin:0;padding:40px 0}.page-header h1{margin:0}.page-header .btn-aligned{float:right;margin-left:8px}.page-header .btn-aligned.pull-left{margin-left:0;margin-right:8px}@media screen and (min-width:992px){.page-header .container>.row h1{margin-top:-3px}.page-header .container>.row .row{margin-top:5px}}@media screen and (max-width:991px){.container h1{font-size:22.5px}.container .btn-icon .material-icon{width:24px;height:24px;margin:-3px 0;font-size:24px;line-height:24px}}.go-back-sm .material-icon,.header-stats .list-inline li .status-icon{width:18px;height:18px;font-size:18px;line-height:18px;position:relative}.page-breadcrumbs{margin-bottom:20px}.page-breadcrumbs .breadcrumb{background:0 0;margin:-20px 0 0;padding:0;overflow:auto}.page-breadcrumbs .breadcrumb li,.page-breadcrumbs .breadcrumb li:before{display:block;float:left}.page-breadcrumbs .breadcrumb li:before{margin-left:4px}.go-back-sm{margin-top:-20px;margin-bottom:20px}.go-back-sm .material-icon{top:-1px}.page-breadcrumbs .go-back-sm{margin-bottom:0}.header-stats{margin-top:20px;margin-bottom:-20px}.header-stats .list-inline{margin-bottom:0}.header-stats .list-inline li{margin-bottom:0;margin-right:8px;overflow:auto;vertical-align:top}.header-stats .list-inline li .status-icon{margin-right:4px;bottom:1px}.header-stats .list-inline li>.icon-legend,.header-stats .list-inline li>.material-icon{float:left}.header-stats .list-inline li>.material-icon{margin-right:4px;position:relative;top:3px}.page-header .page-tabs{margin-bottom:-40px}.page-header .page-tabs .nav>li{margin:0}.page-header .page-tabs .nav>li>a{border-radius:0}.page-tabs ul{display:block;overflow-x:auto}.page-tabs ul a,.page-tabs ul li{display:inline-block;float:none}.page-tabs ul a .material-icon,.page-tabs ul li .material-icon{margin-right:6px}@media screen and (max-width:767px){.page-tabs .container{padding-left:0;padding-right:0}.page-tabs li a{padding-left:26.6px;padding-right:26.6px}.header-stats+.page-tabs{margin-top:30px}}.title-edit-form{margin-bottom:12px}@media screen and (max-width:991px){.header-stats .list-inline{font-size:12px}.header-stats .list-inline li>.material-icon{top:2px}.panel-message-body{text-align:center}.panel-message-body .message-icon{margin:30px}.panel-message-body .message-icon .material-icon{font-size:160px}}@media screen and (min-width:992px){.panel-message-body{padding:20px 20px 30px}.panel-message-body .message-icon{float:left}.panel-message-body .message-icon .material-icon{font-size:50px}.panel-message-body .message-body{margin-left:65px;margin-top:10px}.panel-message-body .message-body .lead{margin-bottom:0}.panel-message-body .message-body .help-block{margin-top:13.2px}}.panel-body-loading{padding:0;text-align:center}.misago-footer{margin-bottom:50px}.misago-footer .noscript-message .material-icon{position:relative;bottom:1px;font-size:18px}.ui-preview{color:#eee;-webkit-animation:ui-preview-animation 1s linear infinite;-o-animation:ui-preview-animation 1s linear infinite;animation:ui-preview-animation 1s linear infinite}@keyframes ui-preview-animation{0%,100%{opacity:1;filter:alpha(opacity=100)}50%{opacity:.1;filter:alpha(opacity=10)}}.ui-preview-text{background:#eee;border-radius:100px;display:inline-block;height:14px;position:relative;top:3px}.ui-preview-paragraph .ui-preview-text{margin-right:6px}.ui-preview-paragraph .ui-preview-text:last-child{margin-right:0}.ui-preview-img{background:#eee;border-radius:5px}.pager-undercontent{margin-top:-20px}@media screen and (min-width:992px){.pager-more .btn{padding-left:20px;padding-right:20px}}.misago-pagination{overflow:auto}.misago-pagination .pagination{float:left;margin:0 10px 0 0}.misago-pagination .pagination li>a,.misago-pagination .pagination li>span{padding:2px}.misago-pagination .pagination .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.misago-pagination p{float:left;margin:0;padding:7px}.panel-poll h2{margin-top:0}.poll-select-choice .btn,.poll-select-choice .btn:active,.poll-select-choice .btn:focus,.poll-select-choice .btn:hover{background:0 0;border:transparent;-webkit-box-shadow:none;box-shadow:none;margin:6px 0;padding:0 0 0 6px;outline:0;text-align:left}.poll-select-choice .btn .material-icon{margin-right:6px;height:28px;width:28px;font-size:28px;line-heigh:28px;color:#eee}.poll-select-choice .btn.btn-selected .material-icon{color:#651fff}.poll-help{font-size:12px}.poll-chart-selected .material-icon{margin-right:4px;position:relative;bottom:1px;height:14px;width:14px;color:#43a047;font-size:14px;line-heigh:14px}.poll-options{margin-bottom:0}.user-status.user-banned .status-icon{color:#f44336}.user-status.user-online .status-icon{color:#43a047}.user-status.user-offline .status-icon{color:#777}.item-title,a.item-title:active,a.item-title:hover,a.item-title:link,a.item-title:visited{color:#212121;font-weight:700}.user-card-small-avatar img{width:100%;height:auto}@media screen and (min-width:768px){.user-card{text-align:center}.user-card-small-avatar{display:none}}@media screen and (max-width:767px){.poll-options{margin-top:-6px}.poll-options .btn{margin:6px 0}.user-card-avatar{display:none}}.toolbar{display:block;margin-bottom:20px}.toolbar.toolbar-bottom{margin-top:10px;margin-bottom:20px}.toolbar>h3{font-size:18px}.toolbar>p{padding:6px 0;text-align:center}@media screen and (min-width:992px){.toolbar-left{float:left;margin-right:16px}.toolbar-right{float:right;margin-left:16px}.toolbar .toolbar-bottom>.form-control,.toolbar .toolbar-control{margin:0;width:auto}.toolbar>h3,.toolbar>p{min-height:34px;margin:0;padding-top:6px;text-align:left}.toolbar>p{padding-top:7px}}.row-toolbar .toolbar-vertical-spacer{margin-top:10px}.row-toolbar-bottom-margin{margin-bottom:20px}@media screen and (min-width:768px){.row-toolbar p{padding-top:7px}}abbr{border:none!important}.item-title{text-decoration:none}a.item-title:hover{text-decoration:underline}.message-line{text-align:center}.message-line .material-icon{margin-right:6.67px;font-size:20px;line-height:20px;height:20px;width:20px}.misago-markup h1,.misago-markup h2,.misago-markup h3,.misago-markup h4,.misago-markup h5,.misago-markup h6{margin-top:40px}.misago-markup blockquote>*,.misago-markup>*{margin:20px 0}.misago-markup blockquote>:first-child,.misago-markup>:first-child{margin-top:0}.misago-markup blockquote>:last-child,.misago-markup>:last-child{margin-bottom:0}.misago-markup img{max-width:100%;max-height:500px}.misago-markup .quote-block,.misago-markup blockquote{background:#eee;border:none;font-size:14px}.misago-markup .quote-block .quote-heading,.misago-markup blockquote .quote-heading{padding:10px 20px;font-size:12px;font-weight:700}.misago-markup .quote-body{margin:0;padding:20px}.misago-markup .quote-body>.quote-block,.misago-markup .quote-body>blockquote{border:1px solid #dadada}.misago-markup ul,.misago-markup ul li{list-style-type:square}.misago-markup ol,.misago-markup ol li{list-style-type:decimal}.misago-markup pre{background:#eee;border:none;padding:10px;overflow:hidden;color:#000}.misago-markup pre code.hljs{margin:-10px;padding:9.5px}.modal-change-avatar .modal-avatar-index .avatar-preview{border-radius:6px;margin:0 auto;overflow:hidden;position:relative;width:200px;height:200px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader{display:none;position:absolute;top:50px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader .loader-spinning-wheel{border-width:10px;border-color:#fff transparent;width:100px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview.preview-loading img{opacity:.33;filter:alpha(opacity=33)}.modal-change-avatar .modal-avatar-index .avatar-preview.preview-loading .loader{display:block}@media (max-width:699px){.modal-change-avatar .modal-avatar-index .avatar-preview{margin-bottom:20px;width:150px;height:150px}.modal-change-avatar .modal-avatar-index .avatar-preview img{width:150px;height:150px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader{top:25px;height:100px}.modal-change-avatar .modal-avatar-index .avatar-preview .loader .loader-spinning-wheel{width:100px;height:100px}}.modal-change-avatar .modal-avatar-index .btn{text-align:left}.modal-change-avatar .modal-avatar-upload{text-align:center}.modal-change-avatar .modal-avatar-upload .btn-pick-file{background:0 0;border:2px solid #eee;border-radius:6px;padding:10px 24px;-webkit-box-shadow:none;box-shadow:none;color:#777;font-size:18px;text-align:center}.modal-change-avatar .modal-avatar-upload .btn-pick-file>.material-icon{display:block;margin:0 auto 13.2px;font-size:50px;width:50px;height:50px}.modal-change-avatar .modal-avatar-upload .btn-pick-file:active,.modal-change-avatar .modal-avatar-upload .btn-pick-file:hover{border-color:#777}.modal-change-avatar .modal-avatar-upload .text-muted{margin-top:13.2px}.modal-change-avatar .modal-avatar-upload .upload-progress img{border-radius:4px;margin-bottom:20px;max-height:80px;width:auto}.modal-change-avatar .modal-avatar-upload .upload-progress .progress{width:70%;margin:0 auto}.modal-avatar-crop .crop-form{margin:0 auto}.modal-avatar-crop .crop-form .cropit-image-zoom-input{margin-top:10px;-webkit-appearance:none;border:1px solid #fff;width:100%}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-webkit-slider-runnable-track{width:100%;height:8px;background:#eee;border:none;border-radius:3px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:20px;width:20px;border-radius:50%;background:#777;margin-top:-6px}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus{outline:0}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-webkit-slider-runnable-track{background:#eee}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-moz-range-track{width:100%;height:8px;background:#eee;border:none;border-radius:4px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-moz-range-thumb{border:none;height:20px;width:20px;border-radius:50%;background:#777}.modal-avatar-crop .crop-form .cropit-image-zoom-input:-moz-focusring{outline:#fff solid 1px;outline-offset:-1px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-track{width:100%;height:8px;background:0 0;border-color:transparent;border-width:8px 0;color:transparent}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-fill-lower{background:#eee;border-radius:16px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-fill-upper{background:#eee;border-radius:16px}.modal-avatar-crop .crop-form .cropit-image-zoom-input::-ms-thumb{border:none;height:20px;width:20px;border-radius:50%;background:#777}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-ms-fill-lower{background:#eee}.modal-avatar-crop .crop-form .cropit-image-zoom-input:focus::-ms-fill-upper{background:#eee}.modal-change-avatar .modal-avatar-gallery{padding-bottom:0}.modal-change-avatar .modal-avatar-gallery .avatars-gallery{margin-bottom:20px}.modal-change-avatar .modal-avatar-gallery .avatars-gallery h3{margin-top:0}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .row{margin-bottom:10px}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn{border-radius:6px;border:2px solid #eee;background:0 0;padding:2px;position:relative}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:focus,.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:hover{border-color:#777}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn.avatar-selected,.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn:active{border-color:#651fff}.modal-change-avatar .modal-avatar-gallery .avatars-gallery-images .btn img{border-radius:4px;width:100%;height:auto}.category-main .read-status .material-icon{color:#eee}.category-main .read-status.item-new .material-icon{color:#651fff}.category-last-thread .media-heading a{display:inline-block;overflow:hidden;white-space:nowrap;width:290px;text-overflow:ellipsis;vertical-align:top}@media screen and (max-width:991px){.category-last-thread .media-heading a{width:275px}}@media screen and (max-width:767px){.category-last-thread .media-heading a{width:260px}}.category-thread-message .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.category-thread-message p{margin:0}.list-inline.subcategories-list{overflow:auto;margin-top:-10px}.list-inline.subcategories-list li{display:block;float:left}.list-inline.subcategories-list li a,.list-inline.subcategories-list li a:active,.list-inline.subcategories-list li a:focus,.list-inline.subcategories-list li a:hover,.list-inline.subcategories-list li a:link,.list-inline.subcategories-list li a:visited{background-color:#eee;border:1px solid #e2e2e2;border-radius:4px;display:inline-block;margin-top:10px;padding:6px 12px;color:#555}#posting-placeholder .first-row .form-control,.editor-border{border:1px solid #c8c8c8}.list-inline.subcategories-list li a:active,.list-inline.subcategories-list li a:hover,.list-inline.subcategories-list li:focus{background-color:#e2e2e2;color:#212121;text-decoration:none}.row.subcategories-list .btn{margin-top:20px;text-align:left}#posting-placeholder{background-color:#eee;display:none;margin-top:30px;margin-bottom:-30px;padding:20px 0;transition:height .3s}#posting-placeholder.slide-in{display:block}#posting-placeholder .first-row{margin-bottom:20px}#posting-placeholder .first-row .posting-options .btn{padding-top:4px;padding-bottom:4px}#posting-placeholder .first-row .posting-options .btn .btn-text{margin-left:5px;position:relative;top:1px}#posting-placeholder .first-row .posting-options .material-icon{width:14px;height:24px;margin-right:0;position:relative;top:5px;font-size:14px;line-height:14px;text-align:center}.posting-ui-preview{padding:20px 0;position:relative}.posting-ui-preview .form-control{box-shadow:none;resize:none}.posting-loader{text-align:center}.posting-loader .loader{height:100px}.posting-loader .loader .loader-spinning-wheel{width:100px;height:100px}.posting-message{text-align:center}.posting-message .material-icon{margin-right:6.67px;position:relative;top:-1px;width:28px;height:28px;font-size:28px;line-height:28px}.posting-message .message-body p{font-size:18px}.editor-border{background-color:#fff;border-radius:4px}.editor-border .form-control{border:none;resize:none}.editor-border .form-control,.editor-border .form-control:active,.editor-border .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.editor-footer{border-top:1px solid #c8c8c8;padding:6px 12px;overflow:auto}.editor-footer .pull-left{margin-right:12px}.editor-footer .pull-right{margin-left:12px}.editor-footer .btn-icon .material-icon{margin-bottom:-2px}@media screen and (max-width:991px){.editor-footer .buttons-list{float:none!important;margin:0 0 10px}.editor-footer .buttons-list .btn{display:inline-block;float:none!important;margin:6.67px}.editor-footer .btn-protect .btn-text{margin-left:10px}.editor-footer .btn-protect .material-icon{position:relative;bottom:2px;width:14px;height:14px;font-size:14px;line-height:14px}}@media screen and (min-width:768px) and (max-width:991px){.buttons-list .btn:first-child{margin-left:0}}@media screen and (max-width:767px){.buttons-list{text-align:center}.buttons-list .btn-protect{display:block;float:none!important;width:100%;margin:10px 0 0}}.editor-attachments-list{margin:0;padding:0}.editor-attachments-list li{margin:0}.editor-attachment-complete{border-top:1px solid #c8c8c8;padding:6px 12px 6px 0}.editor-attachment-complete .editor-attachment-image{float:left;width:50px}.editor-attachment-complete .editor-attachment-image a{background-size:cover;background-position:center;border-radius:3px;display:block;margin:0 auto;width:36px;height:36px}.editor-attachment-complete .editor-attachment-icon{float:left;width:50px;text-align:center}.editor-attachment-complete .editor-attachment-icon .material-icon{position:relative;top:2px;height:28px;width:28px;font-size:28px;line-height:28px}.editor-attachment-complete .editor-attachment-details{margin-left:50px}.editor-attachment-complete .editor-attachment-details h4,.editor-attachment-complete .editor-attachment-details p{margin:0;padding:0;font-size:14px}.editor-attachment-complete .editor-attachment-details p{margin-top:3px;color:#777;font-size:12px}@media screen and (min-width:768px){.editor-attachment-actions{padding-top:3px}}@media screen and (max-width:767px){.editor-attachment-actions{padding-left:12px;padding-right:12px}}.editor-attachment-error{border-top:1px solid #c8c8c8;padding:6px 12px 6px 0}.editor-attachment-error-icon{float:left;width:50px;text-align:center}.editor-attachment-error-icon .material-icon{position:relative;top:2px;height:28px;width:28px;font-size:28px;line-height:28px}.editor-attachment-error-message{margin-left:50px;padding:6px 0;position:relative}.editor-attachment-error-message h4,.editor-attachment-error-message p{margin:0;padding:0;font-size:14px}.editor-attachment-error-message p{margin-top:3px;font-size:12px}.editor-attachment-error-message .btn{position:absolute;top:9px;right:12px}@media screen and (max-width:767px){.editor-attachment-error-message .btn{display:block;margin-top:10px;position:static}}.editor-attachment-progress-bar{background:#c8c8c8;overflow:auto}.editor-attachment-progress{background:#651fff;float:left;height:1px}.editor-attachment-upload-message{margin:0;padding:6px 12px}#editor-upload-field{position:absolute;left:-1000px;top:-1000px}.atwho-view ul li img{border-radius:3px;margin-right:4.67px;width:20px;height:20px}.participant-card .btn-user,.participant-card .dropdown.open .btn-user{margin-bottom:20px}.participant-card .btn-user,.participant-card .btn-user:focus,.participant-card .btn-user:focus:active,.participant-card .btn-user:hover,.participant-card .dropdown.open .btn-user,.participant-card .dropdown.open .btn-user:focus,.participant-card .dropdown.open .btn-user:focus:active,.participant-card .dropdown.open .btn-user:hover{padding:0;overflow:hidden;text-align:left}.participant-card .btn-user img,.participant-card .dropdown.open .btn-user img{background-color:#fff;width:34px;height:34px;margin-right:8px}.panel-participants p{margin:7px 0 0}.poll-choices-control .list-group-item{padding:0}.poll-choices-control .list-group-item .btn{background:0 0;border:transparent;float:left;margin:0 2px -29px;padding:0;width:28px;height:28px;position:relative;top:3px}.poll-choices-control .list-group-item .btn .material-icon{width:28px;height:28px;font-size:28px;line-height:28px}.poll-choices-control input,.poll-choices-control input:active,.poll-choices-control input:focus{background:0 0;border:none;-webkit-box-shadow:none;box-shadow:none;outline:0;margin-left:30px;padding:6px 12px;width:100%}.posts-list{margin:0;padding:0;clear:both}.posts-list li{list-style:none;margin:0;padding:0}.post-side{font-size:12px}.post-side .media{margin:0}.post-side .poster-avatar{height:36px;width:36px}@media screen and (min-width:992px){.post-side .poster-avatar{margin-top:4px;height:82px;width:82px}}.post-side .media-heading{display:block;margin:-1px 0 0;font-size:14px}.post-side .media-heading .user-status{margin-left:2px}.post-heading .pull-right,.post-side .pull-right{margin-left:16px}@media screen and (min-width:992px){.post-side .media-heading{margin-top:3px;font-size:18px}.post-side .media-heading .user-status{display:none}.post-side .user-title{margin-top:4px;margin-bottom:5px}.post-side .user-postcount,.post-side .user-status{display:block}.post-side .pull-right{display:none}}.post-heading{height:36px}@media screen and (max-width:991px){.post-heading{margin-top:10px}.post-heading .pull-right{display:none}}.post-heading .label{margin-top:6px;font-size:14px;font-weight:400}.post-heading .label-unread{color:#fff}.post-body{padding-top:20px;padding-bottom:30px}.post-status-message{overflow:auto}.post-status-message .material-icon{float:left;font-size:28px;line-height:28px}.post-status-message p{margin:4px 0 0 36px}.post-status-best-answer{background-color:#43a047;color:#fff}.post-status-hidden{background-color:#f44336;color:#fff}.post-status-unapproved{background-color:#651fff;color:#fff}.post-status-protected{background-color:#555;color:#fff}.post-footer .pull-left{margin-right:16px}.post-footer .pull-right{margin-left:16px}.post-footer p{padding-top:7px;padding-bottom:6px;margin-bottom:0}.post-attachments{padding:6px 16px}.post-attachments .row>div{margin:10px 0}.post-attachments .post-attachment-preview{float:left;height:40px;width:40px;text-align:center}.post-attachments .post-attachment{margin-left:52px}.post-attachments .post-thumbnail{display:block;background-size:cover;background-position:center;border-radius:3px;width:40px;height:40px}.post-attachments .material-icon{width:28px;height:28px;position:relative;top:5px;font-size:28px;line-height:28px}.post-attachments .material-icon:active,.post-attachments .material-icon:focus,.post-attachments .material-icon:hover,.post-attachments .material-icon:link,.post-attachments .material-icon:visited{color:#222;text-decoration:none}.post-attachments .post-attachment-description{margin:0;padding:0;color:#777;font-size:12px}.post-feed .post-side .media-heading{margin:0;font-size:14px}.post-feed .post-side .user-title{margin:0;font-size:12px}.post-feed .post-side .btn{display:inline-block}.post-feed .post-side img{margin-top:0;width:36px;height:36px}.post-feed .post-heading{height:auto;margin:10px 0}.post-feed .post-heading .btn{margin-right:16px;max-width:100%;text-align:left;white-space:normal}.posts-list .event{margin-bottom:20px;color:#777}.posts-list .event .media{margin-top:5px}.posts-list .event-label .label-unread{background-color:#43a047;color:#fff}.posts-list .event .text-right{padding-right:0;text-align:right}.posts-list .event .text-right .material-icon{margin-right:-12px;height:28px;width:28px;font-size:28px;line-height:28px;text-align:center}.posts-list .event .text-left{padding-left:24px}.posts-list .event .event-message{margin-bottom:5px;font-size:18px}.posts-list .event .event-info{margin:0;font-size:12px}.posts-list .event .event-info li{margin-right:12px}.posts-list .event .event-info li:last-child{margin-right:0}.posts-list .event .event-controls .btn-link{border:0;margin:0 12px 0 0;padding:0;font-size:12px}.posts-list .event .event-controls .btn-link:last-child{margin-right:0}@media screen and (max-width:767px){.posts-list .event .text-right{width:28px;text-align:right}.posts-list .event .text-left:first-child{padding-left:0}.posts-list .event .event-info{margin-top:5px}.posts-list .event .event-controls{clear:both;margin-top:5px}.posts-list .event .event-controls .btn-link{margin-right:20px;font-size:14px}.posts-list .event .event-controls .btn-link:last-child{margin-right:0}.post-changelog-toolbar .post-change-label{text-align:center}}.post-changelog-diff{padding:0;margin:0}.post-changelog-diff .list-unstyled{padding:0;margin:5px 0}.post-changelog-diff .diff-item{padding:5px 10px}.post-changelog-diff .diff-item-sub{color:#f44336}.post-changelog-diff .diff-item-add{color:#43a047}.post-changelog-toolbar .row{margin-left:-12px;margin-right:-12px}@media screen and (max-width:767px){.page-error .message-panel,.page-message .message-panel{text-align:center}.page-error .message-icon,.page-message .message-icon{margin:30px;font-size:80px}}@media screen and (min-width:768px){.page-error .message-panel,.page-message .message-panel{margin:60px auto;max-width:779.35px;overflow:auto}.page-error .message-icon,.page-message .message-icon{float:left}.page-error .message-icon .material-icon,.page-message .message-icon .material-icon{font-size:80px}.page-error .message-body,.page-message .message-body{margin-top:16px;margin-left:100px;font-size:18px}.page-error .message-body p.lead,.page-message .message-body p.lead{font-size:36px}}.threads-list{margin-bottom:20px}.threads-list .thread-title,.threads-list .thread-title:active,.threads-list .thread-title:focus,.threads-list .thread-title:hover,.threads-list .thread-title:link,.threads-list .thread-title:visited{font-size:18px;font-weight:400}.thread-last-action .media-body,.thread-last-action .media-left{padding-top:2px}.threads-list .thread-options{padding-top:5px}.threads-list .thread-main .media-left{padding-top:2px;padding-bottom:1px}.threads-list .thread-details-top{overflow-x:auto;white-space:nowrap}.threads-list .thread-details-bottom div>a,.threads-list .thread-details-bottom div>span,.threads-list .thread-details-top>a,.threads-list .thread-details-top>span{margin-right:16px;font-size:12px;font-weight:400}.threads-list .thread-details-bottom div>a>.material-icon,.threads-list .thread-details-bottom div>span>.material-icon,.threads-list .thread-details-top>a>.material-icon,.threads-list .thread-details-top>span>.material-icon{position:relative;top:-1px}.threads-list .thread-details-bottom div>a .detail-text,.threads-list .thread-details-bottom div>span .detail-text,.threads-list .thread-details-top>a .detail-text,.threads-list .thread-details-top>span .detail-text{margin-left:2px}@media screen and (max-width:991px){.threads-list .thread-details-bottom div>a,.threads-list .thread-details-bottom div>span,.threads-list .thread-details-top>a,.threads-list .thread-details-top>span{margin-right:12px}}@media screen and (max-width:767px){.threads-list .thread-details-bottom{margin-top:8px}.threads-list .thread-details-top{margin-bottom:6px}}@media screen and (min-width:768px){.threads-list .thread-details-top{margin-left:50px}}.thread-last-action{padding-top:1px}.thread-last-action .thread-last-poster{display:block}.thread-options-xs{margin-top:-10px;margin-bottom:-20px;position:relative;top:5px}.thread-options-xs .btn{padding:0 2px;font-size:12px}.thread-options-xs .btn .material-icon{width:14px;height:14px;font-size:14px;line-height:14px}.threads-diff-message{padding:0}.threads-diff-message .btn{border-bottom-left-radius:0;border-bottom-right-radius:0;padding:10px 16px;width:100%;overflow:none;text-align:left}.threads-diff-message .btn .material-icon{margin-right:4px;width:24px;font-size:24px;height:24px}@media screen and (max-width:991px){.threads-diff-message .btn{text-align:center;white-space:normal;word-wrap:break-word}.threads-diff-message .btn .material-icon{display:none}}.threads-list .thread-preview .ui-preview-text{margin-right:16px}.threads-list .thread-preview .thread-details-bottom div .ui-preview-text,.threads-list .thread-preview .thread-details-top .ui-preview-text{height:9.6px}.threads-list .thread-preview .thread-details-bottom{margin-top:6px}.threads-list .thread-preview .thread-details-top{margin-bottom:6px}.threads-list .thread-busy .thread-row{-webkit-animation:thread-busy-animation .6s linear infinite;-o-animation:thread-busy-animation .6s linear infinite;animation:thread-busy-animation .6s linear infinite}@keyframes thread-busy-animation{0%,100%{opacity:.2;filter:alpha(opacity=20)}50%{opacity:.5;filter:alpha(opacity=50)}}.btn-danger.disabled,.btn-danger.disabled:active,.btn-danger.disabled:hover,.btn-danger:disabled,.btn-danger:disabled:hover,.btn-default.btn-outline.disabled,.btn-default.btn-outline.disabled:active,.btn-default.btn-outline.disabled:hover,.btn-default.btn-outline:disabled,.btn-default.btn-outline:disabled:hover,.btn-default.disabled,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:disabled,.btn-default:disabled:hover,.btn-primary.disabled,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:disabled,.btn-primary:disabled:hover,.btn-success.disabled,.btn-success.disabled:active,.btn-success.disabled:hover,.btn-success:disabled,.btn-success:disabled:hover,.navbar-misago .btn-register.disabled,.navbar-misago .btn-register.disabled:active,.navbar-misago .btn-register.disabled:hover,.navbar-misago .btn-register:disabled,.navbar-misago .btn-register:disabled:hover,.navbar-misago .btn-sign-in.disabled,.navbar-misago .btn-sign-in.disabled:active,.navbar-misago .btn-sign-in.disabled:hover,.navbar-misago .btn-sign-in:disabled,.navbar-misago .btn-sign-in:disabled:hover,.page-header .btn-outline.btn-default.disabled,.page-header .btn-outline.btn-default.disabled:active,.page-header .btn-outline.btn-default.disabled:hover,.page-header .btn-outline.btn-default:disabled,.page-header .btn-outline.btn-default:disabled:hover,.page-header .btn-outline.btn-primary.disabled,.page-header .btn-outline.btn-primary.disabled:active,.page-header .btn-outline.btn-primary.disabled:hover,.page-header .btn-outline.btn-primary:disabled,.page-header .btn-outline.btn-primary:disabled:hover,.threads-diff-message .btn.disabled,.threads-diff-message .btn.disabled:active,.threads-diff-message .btn.disabled:hover,.threads-diff-message .btn:disabled,.threads-diff-message .btn:disabled:hover{opacity:.25;filter:alpha(opacity=25)}.active-posters li{display:block;overflow:auto}.active-posters .rank-user-avatar{float:left}.active-posters .rank-user{float:left;margin-top:3px}@media screen and (max-width:991px){.active-posters .rank-user-avatar{height:42px}.active-posters .rank-user-avatar img{width:36px;height:36px;position:relative;top:3px}.active-posters .rank-user{margin-left:13.2px;width:40%}}@media screen and (max-width:767px){.active-posters .rank-user{float:none;margin-left:50px;width:auto}.active-posters .user-details{margin-top:3px}.active-posters .user-details .rank-name,.active-posters .user-details .user-title{font-weight:400}}@media screen and (min-width:992px){.active-posters .rank-user{margin-left:16px;width:25%}.active-posters .rank-user .user-name{font-size:18px}}.active-posters .user-details{overflow:auto;font-family:Sans-Serif}.active-posters .user-details .rank-name,.active-posters .user-details .user-status,.active-posters .user-details .user-title{display:block;float:left;margin-right:3px;font-size:12px}@media screen and (min-width:992px){.active-posters .user-details .rank-name,.active-posters .user-details .user-status,.active-posters .user-details .user-title{margin-right:8px}.active-posters .user-details{overflow:visible}.active-posters .user-details .rank-name,.active-posters .user-details .user-title{height:14px;overflow:hidden;position:relative;top:1px;vertical-align:baseline}}.active-posters .user-details .user-title{margin-right:0}.active-posters .user-status{overflow:auto;position:relative;top:1px}.active-posters .user-status span{display:block;float:left}@media screen and (min-width:992px){.active-posters .user-status{height:14px;overflow:hidden}}.active-posters .user-status .status-icon{position:relative}@media screen and (max-width:991px){.active-posters .user-status .status-icon{top:0;width:12px;height:12px;font-size:12px;line-height:12px}}@media screen and (min-width:992px){.active-posters .user-status .status-icon{top:1px;margin-right:3px;width:13px;height:13px;font-size:13px;line-height:13px}}.active-posters .rank-name .ui-preview-text,.active-posters .status-label.ui-preview-text,.active-posters .user-title .ui-preview-text{height:11px;position:relative;top:2px;font-size:11px;line-height:11px}.active-posters .rank-name .ui-preview-text,.active-posters .user-title .ui-preview-text{position:static}.active-posters .rank-position small,.active-posters .rank-position strong,.active-posters .rank-posts-counted small,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total small,.active-posters .rank-posts-total strong{display:block}@media screen and (max-width:991px){.active-posters .rank-position,.active-posters .rank-posts-counted,.active-posters .rank-posts-total{overflow:auto}.active-posters .rank-position small,.active-posters .rank-position strong,.active-posters .rank-posts-counted small,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total small,.active-posters .rank-posts-total strong{float:left;font-size:10.5px}.active-posters .rank-position strong,.active-posters .rank-posts-counted strong,.active-posters .rank-posts-total strong{min-width:30px;margin-right:3px;text-align:right}.active-posters .rank-position .ui-preview-text,.active-posters .rank-posts-counted .ui-preview-text,.active-posters .rank-posts-total .ui-preview-text{height:8px;position:relative;top:-1px;font-size:8px;line-height:8px}}@media screen and (min-width:992px){.active-posters .rank-position,.active-posters .rank-posts-counted,.active-posters .rank-posts-total{float:left;margin-top:3px;width:23%;font-size:18px;text-align:center}.active-posters .rank-position small,.active-posters .rank-posts-counted small,.active-posters .rank-posts-total small{font-size:12px;font-weight:400}}.user-compact-stats .rank-position small,.user-compact-stats .rank-position strong,.user-compact-stats .rank-posts-counted small,.user-compact-stats .rank-posts-counted strong{display:inline-block;float:none}.user-compact-stats .rank-position strong,.user-compact-stats .rank-posts-counted strong{min-width:auto}.user-compact-stats .rank-position small,.user-compact-stats .rank-posts-counted small{margin-right:20px}@media screen and (min-width:768px) and (max-width:991px){.active-posters .rank-position{margin-top:6px}.active-posters .rank-posts-total{display:none}}.page-user-profile .page-header{padding-top:20px}.page-user-profile .page-header .alert{margin-bottom:20px;text-align:center}.page-user-profile .page-header h1{margin-bottom:0}.page-user-profile .page-header .btn-aligned{margin-top:0}@media screen and (max-width:767px){.page-user-profile .page-header,.page-user-profile .page-header .header-stats ul{text-align:center}.page-user-profile .page-header img{clear:both;margin-top:20px;width:100px;height:100px}.page-user-profile .page-header h1{margin-top:20px;font-size:28px}.page-user-profile .page-header .header-stats ul li{display:inline-block;white-space:nowrap}.page-user-profile .profile-side-avatar{display:none}}@media screen and (min-width:768px) and (max-width:991px){.page-user-profile .page-header h1,.page-user-profile .page-header img{float:left}.page-user-profile .page-header img{margin-top:20px;margin-right:24px;width:80px;height:80px}.page-user-profile .page-header h1{position:relative;top:25px}.page-user-profile .page-header .header-stats{margin-left:112px;margin-top:25px;margin-bottom:-45px;position:relative;bottom:50px}}@media screen and (min-width:992px){.page-user-profile .page-header .user-avatar-sm{display:none}.page-user-profile .page-header h1{position:relative;top:5px}.page-user-profile .page-header .header-stats ul li{display:block;float:left}.page-user-profile .profile-side-avatar img{width:100%;height:auto;margin-top:-140px;margin-bottom:20px}.username-history li{display:block;overflow:auto}}.username-history .change-avatar{float:left}.username-history .change-avatar a,.username-history .change-avatar span{margin-right:10px}.username-history .change-avatar a img,.username-history .change-avatar span img{width:42px;height:42px}.username-history .change{min-height:20px;overflow:auto}.username-history .change span{display:block;float:left}.username-history .change .material-icon{margin:0 7px;position:relative;top:4px}.search-footer p{margin-top:20px;color:#555;font-size:12px;text-align:center}@media screen and (min-width:768px){.page-search-form{padding-top:40px}}@media screen and (min-width:992px){.username-history .change-avatar a img,.username-history .change-avatar span img{width:18px;height:18px;position:relative;bottom:1px}.username-history .change-author{float:left;width:30%}.username-history .change{float:left;width:40%}.username-history .change-date{float:left;width:20%}.page-search-form{padding-bottom:40px}.page-search-form h1{position:relative;top:5px}.page-search-form .form-group{margin-bottom:0}}.hljs{display:block;overflow-x:auto;padding:.5em;background:#eee;color:#000}.hljs-addition,.hljs-attribute,.hljs-emphasis,.hljs-link{color:#070}.hljs-emphasis{font-style:italic}.hljs-deletion,.hljs-string,.hljs-strong{color:#d14}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-section,.hljs-title{color:#900}.hljs-class .hljs-title,.hljs-type{color:#458}.hljs-template-variable,.hljs-variable{color:#369}.hljs-bullet{color:#970}.hljs-meta{color:#34b}.hljs-code,.hljs-keyword,.hljs-literal,.hljs-number,.hljs-selector-tag{color:#099}.hljs-regexp{background-color:#fff0ff;color:#808}.hljs-symbol{color:#990073}.hljs-name,.hljs-selector-class,.hljs-selector-id,.hljs-tag{color:#070}.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400}#misago-container,body,html{background:#fff}abbr{outline:0;text-decoration:none}.shadow-2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.shadow-3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.04),0 3px 3px -2px rgba(0,0,0,.06),0 1px 8px 0 rgba(0,0,0,.12)}.shadow-4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.shadow-6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.04),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.06)}.shadow-8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.04),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.06)}.shadow-16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.04),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.06)}.shadow-24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.04),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.06)}.btn-default,.btn-default.disabled,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:disabled,.btn-default:disabled:hover{background:#f5f5f5;border:1px solid #f5f5f5;color:#757575;-webkit-box-shadow:none;box-shadow:none}.btn-default:focus,.btn-default:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default:active,.btn-default:active:focus,.dropdown.open .btn-default,.dropdown.open .btn-default:active:focus,.dropdown.open .btn-default:focus,.dropdown.open .btn-default:hover{background:#bdbdbd;border:1px solid #bdbdbd;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-loading,.btn-default.btn-loading:active,.btn-default.btn-loading:active:focus,.btn-default.btn-loading:disabled,.btn-default.btn-loading:disabled:hover,.btn-default.btn-loading:focus,.btn-default.btn-loading:hover{color:transparent}.btn-primary,.btn-primary.disabled,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:disabled,.btn-primary:disabled:hover{background:#a36eff;border:1px solid #a36eff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary:focus,.btn-primary:hover{background:#b388ff;border:1px solid #b388ff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary:active,.btn-primary:active:focus,.dropdown.open .btn-primary,.dropdown.open .btn-primary:active:focus,.dropdown.open .btn-primary:focus,.dropdown.open .btn-primary:hover{background:#823bff;border:1px solid #823bff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-loading,.btn-primary.btn-loading:active,.btn-primary.btn-loading:active:focus,.btn-primary.btn-loading:disabled,.btn-primary.btn-loading:disabled:hover,.btn-primary.btn-loading:focus,.btn-primary.btn-loading:hover{color:transparent}.btn-success,.btn-success.disabled,.btn-success.disabled:active,.btn-success.disabled:hover,.btn-success:disabled,.btn-success:disabled:hover{background:#00c853;border:1px solid #00c853;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success:focus,.btn-success:hover{background:#00af48;border:1px solid #00af48;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success:active,.btn-success:active:focus,.dropdown.open .btn-success,.dropdown.open .btn-success:active:focus,.dropdown.open .btn-success:focus,.dropdown.open .btn-success:hover{background:#007c33;border:1px solid #007c33;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-success.btn-loading,.btn-success.btn-loading:active,.btn-success.btn-loading:active:focus,.btn-success.btn-loading:disabled,.btn-success.btn-loading:disabled:hover,.btn-success.btn-loading:focus,.btn-success.btn-loading:hover{color:transparent}.btn-danger,.btn-danger.disabled,.btn-danger.disabled:active,.btn-danger.disabled:hover,.btn-danger:disabled,.btn-danger:disabled:hover{background:#ef5350;border:1px solid #ef5350;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger:focus,.btn-danger:hover{background:#ff8a80;border:1px solid #ff8a80;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger:active,.btn-danger:active:focus,.dropdown.open .btn-danger,.dropdown.open .btn-danger:active:focus,.dropdown.open .btn-danger:focus,.dropdown.open .btn-danger:hover{background:#d32f2f;border:1px solid #d32f2f;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-danger.btn-loading,.btn-danger.btn-loading:active,.btn-danger.btn-loading:active:focus,.btn-danger.btn-loading:disabled,.btn-danger.btn-loading:disabled:hover,.btn-danger.btn-loading:focus,.btn-danger.btn-loading:hover{color:transparent}.btn-default.btn-outline,.btn-default.btn-outline.disabled,.btn-default.btn-outline.disabled:active,.btn-default.btn-outline.disabled:hover,.btn-default.btn-outline:disabled,.btn-default.btn-outline:disabled:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:#616161;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline:focus,.btn-default.btn-outline:hover{background:#bdbdbd;border:1px solid #bdbdbd;color:#424242;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline:active,.btn-default.btn-outline:active:focus,.dropdown.open .btn-default.btn-outline,.dropdown.open .btn-default.btn-outline:active:focus,.dropdown.open .btn-default.btn-outline:focus,.dropdown.open .btn-default.btn-outline:hover{background:#eee;border:1px solid #eee;color:#212121;-webkit-box-shadow:none;box-shadow:none}.btn-default.btn-outline.btn-loading,.btn-default.btn-outline.btn-loading:active,.btn-default.btn-outline.btn-loading:active:focus,.btn-default.btn-outline.btn-loading:disabled,.btn-default.btn-outline.btn-loading:disabled:hover,.btn-default.btn-outline.btn-loading:focus,.btn-default.btn-outline.btn-loading:hover{background:#e0e0e0;border:1px solid #e0e0e0;color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline,.btn-primary.btn-outline.disabled,.btn-primary.btn-outline.disabled:active,.btn-primary.btn-outline.disabled:hover,.btn-primary.btn-outline:disabled,.btn-primary.btn-outline:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline.disabled,.btn-primary.btn-outline.disabled:active,.btn-primary.btn-outline.disabled:hover,.btn-primary.btn-outline:disabled,.btn-primary.btn-outline:disabled:hover{opacity:.25;filter:alpha(opacity=25)}.btn-primary.btn-outline:focus,.btn-primary.btn-outline:hover{background:#651fff;border:1px solid #651fff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline:active,.btn-primary.btn-outline:active:focus,.dropdown.open .btn-primary.btn-outline,.dropdown.open .btn-primary.btn-outline:active:focus,.dropdown.open .btn-primary.btn-outline:focus,.dropdown.open .btn-primary.btn-outline:hover{background:#6200ea;border:1px solid #6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn-primary.btn-outline.btn-loading,.btn-primary.btn-outline.btn-loading:active,.btn-primary.btn-outline.btn-loading:active:focus,.btn-primary.btn-outline.btn-loading:disabled,.btn-primary.btn-outline.btn-loading:disabled:hover,.btn-primary.btn-outline.btn-loading:focus,.btn-primary.btn-outline.btn-loading:hover{background:#7c4dff;border:1px solid #7c4dff;color:transparent;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in{border-radius:100px}.navbar-misago .btn-sign-in,.navbar-misago .btn-sign-in.disabled,.navbar-misago .btn-sign-in.disabled:active,.navbar-misago .btn-sign-in.disabled:hover,.navbar-misago .btn-sign-in:disabled,.navbar-misago .btn-sign-in:disabled:hover{background:0 0;border:1px solid transparent;color:#e0e0e0;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in:focus,.navbar-misago .btn-sign-in:hover{background:#666060;border:1px solid #666060;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .navbar-misago .btn-sign-in,.dropdown.open .navbar-misago .btn-sign-in:active:focus,.dropdown.open .navbar-misago .btn-sign-in:focus,.dropdown.open .navbar-misago .btn-sign-in:hover,.navbar-misago .btn-sign-in:active,.navbar-misago .btn-sign-in:active:focus{background:#424242;border:1px solid #424242;color:#fff;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-sign-in.btn-loading,.navbar-misago .btn-sign-in.btn-loading:active,.navbar-misago .btn-sign-in.btn-loading:active:focus,.navbar-misago .btn-sign-in.btn-loading:disabled,.navbar-misago .btn-sign-in.btn-loading:disabled:hover,.navbar-misago .btn-sign-in.btn-loading:focus,.navbar-misago .btn-sign-in.btn-loading:hover{background:0 0;border:1px solid transparent;color:transparent;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register{border-radius:100px}.navbar-misago .btn-register,.navbar-misago .btn-register.disabled,.navbar-misago .btn-register.disabled:active,.navbar-misago .btn-register.disabled:hover,.navbar-misago .btn-register:disabled,.navbar-misago .btn-register:disabled:hover{background:0 0;border:1px solid #757575;color:#e0e0e0;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register:focus,.navbar-misago .btn-register:hover{background:#fff;border:1px solid #fff;color:#212121;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .navbar-misago .btn-register,.dropdown.open .navbar-misago .btn-register:active:focus,.dropdown.open .navbar-misago .btn-register:focus,.dropdown.open .navbar-misago .btn-register:hover,.navbar-misago .btn-register:active,.navbar-misago .btn-register:active:focus{background:#bdbdbd;border:1px solid #bdbdbd;color:#212121;-webkit-box-shadow:none;box-shadow:none}.navbar-misago .btn-register.btn-loading,.navbar-misago .btn-register.btn-loading:active,.navbar-misago .btn-register.btn-loading:active:focus,.navbar-misago .btn-register.btn-loading:disabled,.navbar-misago .btn-register.btn-loading:disabled:hover,.navbar-misago .btn-register.btn-loading:focus,.navbar-misago .btn-register.btn-loading:hover{background:0 0;border:1px solid #757575;color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-social-battlenet,.btn-social-battlenet-oauth2,.btn-social-battlenet-oauth2:disabled,.btn-social-battlenet-oauth2:disabled:hover,.btn-social-battlenet:disabled,.btn-social-battlenet:disabled:hover{color:#0e86ca;font-weight:700}.btn-social-bungie,.btn-social-bungie:disabled,.btn-social-bungie:disabled:hover{color:#0096db;font-weight:700}.btn-social-facebook,.btn-social-facebook-app,.btn-social-facebook-app:disabled,.btn-social-facebook-app:disabled:hover,.btn-social-facebook:disabled,.btn-social-facebook:disabled:hover{color:#3b5998;font-weight:700}.btn-social-github,.btn-social-github-enterprise,.btn-social-github-enterprise-org,.btn-social-github-enterprise-org:disabled,.btn-social-github-enterprise-org:disabled:hover,.btn-social-github-enterprise-team,.btn-social-github-enterprise-team:disabled,.btn-social-github-enterprise-team:disabled:hover,.btn-social-github-enterprise:disabled,.btn-social-github-enterprise:disabled:hover,.btn-social-github-team,.btn-social-github-team:disabled,.btn-social-github-team:disabled:hover,.btn-social-github:disabled,.btn-social-github:disabled:hover{color:#000;font-weight:700}.btn-social-gitlab,.btn-social-gitlab:disabled,.btn-social-gitlab:disabled:hover{color:#fc6d26;font-weight:700}.btn-social-google,.btn-social-google-oauth,.btn-social-google-oauth2,.btn-social-google-oauth2:disabled,.btn-social-google-oauth2:disabled:hover,.btn-social-google-oauth:disabled,.btn-social-google-oauth:disabled:hover,.btn-social-google-openidconnect,.btn-social-google-openidconnect:disabled,.btn-social-google-openidconnect:disabled:hover,.btn-social-google-plus,.btn-social-google-plus:disabled,.btn-social-google-plus:disabled:hover,.btn-social-google:disabled,.btn-social-google:disabled:hover{color:#dd4b39;font-weight:700}.btn-social-linkedin,.btn-social-linkedin:disabled,.btn-social-linkedin:disabled:hover{color:#0077b5;font-weight:700}.btn-social-steam,.btn-social-steam:disabled,.btn-social-steam:disabled:hover{color:#5c7e10;font-weight:700}.btn-social-twitter,.btn-social-twitter:disabled,.btn-social-twitter:disabled:hover{color:#1da1f2;font-weight:700}.form-social-auth .row{margin-top:-6px;margin-bottom:-6px}.form-social-auth .btn{margin:6px 0}input.form-control,textarea.form-control{border-color:#ccc;box-shadow:inset 0 0 0 1px #ccc}input.form-control:focus,textarea.form-control:focus{border-color:#66afe9;box-shadow:inset 0 0 0 1px #66afe9}.has-error input.form-control{border-color:#f44336;box-shadow:inset 0 0 0 1px #f44336}.has-error input.form-control:focus{border-color:#f99d97;box-shadow:inset 0 0 0 1px #f99d97}.has-success input.form-control{border-color:#00c853;box-shadow:inset 0 0 0 1px #00c853}.has-success input.form-control:focus{border-color:#2fff85;box-shadow:inset 0 0 0 1px #2fff85}.password-strength{margin-top:10px}.password-strength .text-small{margin-top:4px;color:#616161;font-size:12px}.password-strength .progress{margin:0}.auth-message{background:#651fff;padding:80px 0;box-shadow:0 8px 10px 1px rgba(0,0,0,.04),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.06)}.auth-message p{color:#fff}.auth-message .btn{background:#4527a0;border-color:#4527a0;color:#fff}.auth-message .btn:focus,.auth-message .btn:hover{background:#b388ff;border-color:#b388ff;color:#fff}.auth-message .btn:active{background:#fff;border-color:#fff;color:#651fff}.navbar-misago,.page-header{border-bottom:none}.dropdown-menu{border:none;box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.user-dropdown .dropdown-header strong{display:block;font-size:18px;font-weight:300}.user-dropdown .dropdown-header .user-stats{margin-top:10px;font-size:12px}.user-dropdown .dropdown-header .user-stats .material-icon{margin-right:3px;position:relative;bottom:1px;width:14px;height:14px;font-size:14px}.user-dropdown .guest-preview{padding-bottom:10px}.dropdown-search-loader,.dropdown-search-message{padding:10px 16px;border-top:1px solid #eee}.user-dropdown .badge{background-color:#f44336}.user-dropdown .btn-link:active .badge,.user-dropdown .btn-link:focus .badge,.user-dropdown .btn-link:hover .badge,.user-dropdown a:active .badge,.user-dropdown a:focus .badge,.user-dropdown a:hover .badge{background-color:#fff;color:#f44336}.mobile-dropdown.open{margin:0}.navbar-misago .user-avatar{background:#fff;border-radius:3px}.navbar-misago .brand-link img{border-radius:3px}.dropdown-search-results{border-radius:0 0 4px 4px}.dropdown-search-message{color:#777}.dropdown-search-header{border-top:1px solid #eee;padding:8px 20px;color:#777;font-weight:700}.dropdown-search-thread small,.dropdown-search-user small{color:#777}.dropdown-search-thread:focus small,.dropdown-search-thread:hover small,.dropdown-search-user:focus small,.dropdown-search-user:hover small{color:#FAFAFA}.dropdown-search-thread:active small,.dropdown-search-user:active small{color:#fff}.dropdown-search-thread .dropdown-search-post-content{overflow:hidden;white-space:normal;max-height:47px}.dropdown-search-thread .dropdown-search-post-footer{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dropdown-search-footer{padding-bottom:5px}.dropdown-search-footer:last-child{padding-bottom:0}.dropdown-menu>li.dropdown-search-footer>a{font-size:small}.page-header{background:0 0}.page-header-bg{background:url(../img/page-header.svg) #6200ea;background-size:cover;min-height:8px}@media screen and (min-width:992px){.page-header-bg{margin-bottom:40px}}.page-header h1{color:#fff;font-weight:400}.page-header .btn-outline.btn-default,.page-header .btn-outline.btn-default.disabled,.page-header .btn-outline.btn-default.disabled:active,.page-header .btn-outline.btn-default.disabled:hover,.page-header .btn-outline.btn-default:disabled,.page-header .btn-outline.btn-default:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-default:focus,.page-header .btn-outline.btn-default:hover{background:#651fff;border:1px solid #651fff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .page-header .btn-outline.btn-default,.dropdown.open .page-header .btn-outline.btn-default:active:focus,.dropdown.open .page-header .btn-outline.btn-default:focus,.dropdown.open .page-header .btn-outline.btn-default:hover,.page-header .btn-outline.btn-default:active,.page-header .btn-outline.btn-default:active:focus{background:#6200ea;border:1px solid #6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-default.btn-loading,.page-header .btn-outline.btn-default.btn-loading:active,.page-header .btn-outline.btn-default.btn-loading:active:focus,.page-header .btn-outline.btn-default.btn-loading:disabled,.page-header .btn-outline.btn-default.btn-loading:disabled:hover,.page-header .btn-outline.btn-default.btn-loading:focus,.page-header .btn-outline.btn-default.btn-loading:hover{color:transparent}.page-header .btn-outline.btn-primary,.page-header .btn-outline.btn-primary.disabled,.page-header .btn-outline.btn-primary.disabled:active,.page-header .btn-outline.btn-primary.disabled:hover,.page-header .btn-outline.btn-primary:disabled,.page-header .btn-outline.btn-primary:disabled:hover{background:#f5f5f5;border:1px solid #f5f5f5;color:#424242;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-primary:focus,.page-header .btn-outline.btn-primary:hover{background:#fff;border:1px solid #fff;color:#212121;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .page-header .btn-outline.btn-primary,.dropdown.open .page-header .btn-outline.btn-primary:active:focus,.dropdown.open .page-header .btn-outline.btn-primary:focus,.dropdown.open .page-header .btn-outline.btn-primary:hover,.page-header .btn-outline.btn-primary:active,.page-header .btn-outline.btn-primary:active:focus{background:#212121;border:1px solid #212121;color:#fff;-webkit-box-shadow:none;box-shadow:none}.page-header .btn-outline.btn-primary.btn-loading,.page-header .btn-outline.btn-primary.btn-loading:active,.page-header .btn-outline.btn-primary.btn-loading:active:focus,.page-header .btn-outline.btn-primary.btn-loading:disabled,.page-header .btn-outline.btn-primary.btn-loading:disabled:hover,.page-header .btn-outline.btn-primary.btn-loading:focus,.page-header .btn-outline.btn-primary.btn-loading:hover{color:transparent}.header-stats{color:#ede7f6}.header-stats li a,.header-stats li a:link,.header-stats li a:visited{color:#fff}.header-stats li a:active,.header-stats li a:focus,.header-stats li a:hover{color:#f5f5f5}.header-stats li .user-status.user-banned .status-icon,.header-stats li .user-status.user-offline .status-icon,.header-stats li .user-status.user-online .status-icon{color:#ede7f6}.header-stats li .item-title{color:#fff!important}.page-header .go-back-sm,.page-header .go-back-sm:link,.page-header .go-back-sm:visited{color:#ede7f6}.page-header .go-back-sm:active,.page-header .go-back-sm:focus,.page-header .go-back-sm:hover{color:#f5f5f5}.page-header .breadcrumb,.page-header .breadcrumb a,.page-header .breadcrumb a:link,.page-header .breadcrumb a:visited{color:#ede7f6}.page-header .breadcrumb a:active,.page-header .breadcrumb a:focus,.page-header .breadcrumb a:hover{color:#fff}.page-header .breadcrumb li:before{color:#ede7f6;text-shadow:0 1px 1px #6200ea}.page-header .page-tabs{background-color:transparent;margin-top:33.2px}.page-header .page-tabs li a{font-weight:700}.page-header .page-tabs li a,.page-header .page-tabs li a:link,.page-header .page-tabs li a:visited{background-color:transparent;border-radius:3px 3px 0 0;color:#ede7f6}.list-group,.threads-list .list-group{border-radius:3px}.page-header .page-tabs li a:focus,.page-header .page-tabs li a:hover{background-color:#7c4dff;color:#fff}@media screen and (max-width:991px){.page-header .page-tabs li a:focus,.page-header .page-tabs li a:hover{background-color:transparent;color:#ede7f6}}.page-header .page-tabs li.active a,.page-header .page-tabs li.active a:active,.page-header .page-tabs li.active a:focus,.page-header .page-tabs li.active a:hover,.page-header .page-tabs li.active a:link,.page-header .page-tabs li.active a:visited{background-color:#fff;color:#424242}.misago-footer{background:#fff;margin-top:40px}.misago-footer .footer-content{border-top:1px solid #e6e6e6;padding-top:20px;color:#bdbdbd}@media screen and (max-width:767px){.misago-footer .footer-content{text-align:center}.misago-footer .footer-content .site-footnote{clear:both;margin-bottom:15px}}.misago-footer .footer-content a,.misago-footer .footer-content a:link,.misago-footer .footer-content a:visited{color:#757575}.misago-footer .footer-content a:focus,.misago-footer .footer-content a:hover{color:#212121}.misago-footer .footer-content .misago-branding,.misago-footer .footer-content .misago-branding:link,.misago-footer .footer-content .misago-branding:visited{color:#b388ff}.misago-footer .footer-content .misago-branding:focus,.misago-footer .footer-content .misago-branding:hover{color:#8f67ff}.list-group{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.list-group .list-group-item{border-left-color:#ddd;border-right-color:#ddd}.list-group .list-group-item:first-child{border-top-color:#ddd}.list-group .list-group-item:last-child{border-bottom-color:#ddd}.list-group-item.empty-message{padding-top:20px;padding-bottom:20px;text-align:center}.list-group-item.empty-message p{margin:10px 0}.threads-list .thread-last-action img,.threads-list .thread-main img{border-radius:4px}.threads-list .thread-last-action .thread-last-reply,.threads-list .thread-last-action .thread-last-reply:link,.threads-list .thread-last-action .thread-last-reply:visited{color:#777;font-size:12px}.threads-list .thread-last-action .thread-last-reply:active,.threads-list .thread-last-action .thread-last-reply:focus,.threads-list .thread-last-action .thread-last-reply:hover{color:#555}.threads-list .thread-details-top{color:#777}.threads-list .thread-details-top a,.threads-list .thread-details-top a:link,.threads-list .thread-details-top a:visited{color:#777;font-size:12px}.threads-list .thread-details-top a:active,.threads-list .thread-details-top a:focus,.threads-list .thread-details-top a:hover{color:#212121}.threads-list .thread-details-top .thread-detail-new,.threads-list .thread-details-top .thread-detail-new:link,.threads-list .thread-details-top .thread-detail-new:visited{color:#a0f}.threads-list .thread-details-top .thread-detail-new:active,.threads-list .thread-details-top .thread-detail-new:focus,.threads-list .thread-details-top .thread-detail-new:hover{color:#80c}.threads-list .thread-details-top .thread-detail-pinned-globally{color:#3d5afe}.threads-list .thread-details-top .thread-detail-pinned-locally{color:#8c9eff}.threads-list .thread-details-top .thread-detail-unapproved{color:#ef6c00}.threads-list .thread-details-top a.thread-detail-answered{color:#388e3c}.threads-list .thread-details-top .thread-detail-unapproved-posts{color:#f4511e}.threads-list .thread-details-bottom{color:#777}.threads-list .thread-details-bottom a,.threads-list .thread-details-bottom a:link,.threads-list .thread-details-bottom a:visited{color:#555;font-size:12px}.threads-list .thread-details-bottom a:active,.threads-list .thread-details-bottom a:focus,.threads-list .thread-details-bottom a:hover{color:#212121}.threads-list .thread-read .thread-title,.threads-list .thread-read .thread-title:active,.threads-list .thread-read .thread-title:focus,.threads-list .thread-read .thread-title:hover,.threads-list .thread-read .thread-title:link,.threads-list .thread-read .thread-title:visited{color:#555}.threads-diff-message .btn,.threads-diff-message .btn.disabled,.threads-diff-message .btn.disabled:active,.threads-diff-message .btn.disabled:hover,.threads-diff-message .btn:disabled,.threads-diff-message .btn:disabled:hover{background:#7c4dff;border:1px solid #7c4dff;color:#fff;-webkit-box-shadow:none;box-shadow:none}.threads-diff-message .btn:focus,.threads-diff-message .btn:hover{background:#6200ea;color:#fff;-webkit-box-shadow:none;box-shadow:none}.dropdown.open .threads-diff-message .btn,.dropdown.open .threads-diff-message .btn:active:focus,.dropdown.open .threads-diff-message .btn:focus,.dropdown.open .threads-diff-message .btn:hover,.threads-diff-message .btn:active,.threads-diff-message .btn:active:focus{background:#4d00b7;border:1px solid #4d00b7;color:#fff;-webkit-box-shadow:none;box-shadow:none}.panel,.threads-diff-message .btn,.threads-diff-message .btn:active,.threads-diff-message .btn:focus,.threads-diff-message .btn:focus:active,.threads-diff-message .btn:hover{border:none}.threads-diff-message .btn.btn-loading,.threads-diff-message .btn.btn-loading:active,.threads-diff-message .btn.btn-loading:active:focus,.threads-diff-message .btn.btn-loading:disabled,.threads-diff-message .btn.btn-loading:disabled:hover,.threads-diff-message .btn.btn-loading:focus,.threads-diff-message .btn.btn-loading:hover{color:transparent}.nav-side{-webkit-box-shadow:none;box-shadow:none}.panel-form,.panel-participants,.panel-post{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.nav-side>.list-group-item{margin-bottom:1px}.nav-side>.list-group-item:first-child{border-top:none}.nav-side>.list-group-item:last-child{border-bottom:none}.nav-side>.list-group-item,.nav-side>.list-group-item:link,.nav-side>.list-group-item:visited{background:#f5f5f5;border:none;color:#757575}.nav-side>.list-group-item:active,.nav-side>.list-group-item:focus,.nav-side>.list-group-item:hover{background:#e0e0e0;color:#424242}.nav-side>.list-group-item.active,.nav-side>.list-group-item.active:active,.nav-side>.list-group-item.active:focus,.nav-side>.list-group-item.active:hover,.nav-side>.list-group-item.active:link,.nav-side>.list-group-item.active:visited{background:#651fff;color:#fff;font-weight:700}.nav-side>.list-group-item.active .badge,.nav-side>.list-group-item.active:active .badge,.nav-side>.list-group-item.active:focus .badge,.nav-side>.list-group-item.active:hover .badge,.nav-side>.list-group-item.active:link .badge,.nav-side>.list-group-item.active:visited .badge{background:#651fff;color:#fff}.panel,.username-history .user-avatar{border-radius:4px}.panel .panel-footer,.panel .panel-heading{background:#fff}.panel .panel-title{color:#777}.panel .form-group+.form-group{margin-top:20px}.panel .panel-body>.form-group:first-child{margin-top:10px}.panel fieldset{margin-top:20px}.panel fieldset:first-child{margin-top:0}.panel fieldset legend{border:none;color:#555;font-weight:300}.panel-message-body .message-icon{color:#9575cd}.panel-form{border:1px solid #ddd}.poll-form .panel{margin:0}.category-main .media-left .read-status{background:#eee;border-radius:4px;padding:5px 8px}.category-main .media-left .material-icon{height:14px;width:14px;color:#555;font-size:14px;line-height:14px}.category-main .media-left .read-status.item-new{background-color:#651fff}.category-main .media-left .read-status.item-new .material-icon{color:#fff}@media screen and (min-width:768px){.category-main .media-left{padding:5px 0}}.category-main .media-body{padding-left:12px}.list-group-category-no-description .category-main .media-heading{margin-top:10px}.category-main .media-heading a,.category-main .media-heading a:link,.category-main .media-heading a:visited{color:#333}.category-main .media-heading a:active,.category-main .media-heading a:focus,.category-main .media-heading a:hover{color:#212121}.category-stats{color:#555}.category-main .category-description p{font-size:12px}.category-main .category-description p:last-child{margin-bottom:0}.category-last-thread img{background-color:#fff;border-radius:4px}.category-last-thread .media-heading{margin-bottom:2px}@media screen and (max-width:767px){.category-main .media-body{padding-left:3.33px}.list-group-category-no-description .category-main .media-heading{margin-top:5px}.category-last-thread .media-heading{margin-top:10px}}.category-last-thread .list-inline{color:#777;font-size:12px}.category-last-thread .list-inline a,.category-last-thread .list-inline a:link,.category-last-thread .list-inline a:visited{color:#777}.category-last-thread .list-inline .item-title,.category-last-thread .list-inline .item-title:link,.category-last-thread .list-inline .item-title:visited,.category-last-thread .list-inline a:active,.category-last-thread .list-inline a:focus,.category-last-thread .list-inline a:hover{color:#333}.category-last-thread .list-inline .item-title:active,.category-last-thread .list-inline .item-title:focus,.category-last-thread .list-inline .item-title:hover{color:#212121}.category-thread-message{color:#777}.category-thread-message .material-icon{padding:6px 0;margin-right:3px}@media screen and (max-width:767px){.category-thread-message{padding-top:15px;font-size:12px}.category-thread-message .material-icon{padding:3px 0}.panel-participants p{margin-top:10px;text-align:center}}.panel-participants{border:1px solid #ddd}.participant-card .btn-user{border:0!important}.panel-poll,.panel-post{border:1px solid #ddd}.participant-card .dropdown-header-owner{color:#651fff}.participant-card .dropdown-header-owner .material-icon{width:14px;height:14px;font-size:14px;line-height:14px}.panel-poll .poll-chart,.panel-poll .poll-details{color:#777;font-size:12px}.participant-card .dropdown-header-owner .icon-text{margin-left:4px;position:relative;top:2px}.panel-participants p{color:#777}.panel-poll .poll-select-choices{margin-top:-10px}.panel-poll .poll-details{margin-bottom:20px}.panel-poll .progress{margin-top:6.67px;margin-bottom:5px}.panel-poll .poll-options,.posting-message .btn{margin-top:20px}.posting-message .material-icon{color:#9575cd}.panel-post{background:#fff}.post-side{color:#777}.post-side .poster-avatar{border-radius:4px}.post-side .user-title,.post-side .user-title a,.post-side .user-title a:active,.post-side .user-title a:focus,.post-side .user-title a:hover,.post-side .user-title a:link,.post-side .user-title a:visited{color:#555}.post-heading .label-unread{background-color:#a0f;margin-right:16px}.post-heading .label-protected{background-color:transparent;margin-left:24px;position:relative;top:1px;color:#bdbdbd}.post-heading .label-protected .material-icon{margin-right:2px;position:relative;top:-1px;font-size:16px;line-height:16px}.post-heading>.btn-link{padding-left:0;padding-right:0}.post-heading>.btn-link,.post-heading>.btn-link:link,.post-heading>.btn-link:visited{color:#777}.post-heading>.btn-link:active,.post-heading>.btn-link:focus,.post-heading>.btn-link:focus:active,.post-heading>.btn-link:hover{color:#212121;text-decoration:none}.post-heading .btn-see-edits{margin-left:24px}.post-status-message{border-radius:4px;margin-top:10px;padding:6px 12px}.post-body:last-child{padding-bottom:10px}.post-attachments{background-color:#f2f2f2;border:none;border-radius:4px;margin-bottom:30px}.post-attachments:last-child{margin-bottom:10px}@media screen and (max-width:767px){.post-status-message{font-size:12px}.post-status-message .material-icon{margin-top:3px}.post-status-message p{margin-top:0}.post-attachments{border-radius:0;margin:0 -15px 20px}}.misago-markup img,.user-card-avatar img,.user-card-small-avatar img{border-radius:4px}.post-footer>.btn-link{padding-left:0;padding-right:0}.post-footer>.btn-link,.post-footer>.btn-link:link,.post-footer>.btn-link:visited{color:#777}.post-footer>.btn-link:active,.post-footer>.btn-link:focus,.post-footer>.btn-link:focus:active,.post-footer>.btn-link:hover{color:#212121;text-decoration:none}.post-footer p{color:#777;font-size:12px}.post-body-hidden,.post-body-invalid{padding-top:10px;padding-bottom:10px}.post-body-hidden .lead,.post-body-invalid .lead{margin-bottom:10px}.post-body-hidden .text-muted,.post-body-invalid .text-muted{margin-bottom:0;font-size:12px}.post-hidden{opacity:.75;filter:alpha(opacity=75)}.post-feed .panel-body{padding-bottom:0}.post-feed .post-body{position:relative;padding-top:0;padding-bottom:20px;max-height:300px;overflow-y:hidden}.post-feed .post-body:after{box-shadow:0 0 16px 16px #fff;display:block;position:absolute;bottom:0;height:0;width:100%;content:'-';color:transparent}.posts-list .event .event-label{margin-bottom:5px}.posts-list .event .label-unread{background-color:#a0f;color:#fff}.posts-list .event-info .btn-link,.posts-list .event-info a,.posts-list .event-info a:link,.posts-list .event-info a:visited{color:#555}.posts-list .event-info .btn-link:active,.posts-list .event-info .btn-link:focus,.posts-list .event-info .btn-link:focus:active,.posts-list .event-info a:active,.posts-list .event-info a:focus,.posts-list .event-info a:focus:active{color:#212121}.posts-list .event-hidden{opacity:.33;filter:alpha(opacity=33)}.user-card{background:#f5f5f5}.user-card-avatar{margin:20px 0}.user-card-avatar img{width:150px;height:150px}.user-card-username a,.user-card-username a:active,.user-card-username a:focus,.user-card-username a:hover,.user-card-username a:link,.user-card-username a:visited{color:#212121;font-size:18px;font-weight:700}.user-card-title a,.user-card-title a:link,.user-card-title a:visited,.user-card-title span{color:#555}.user-card-title a:active,.user-card-title a:focus,.user-card-title a:hover{color:#212121}@media screen and (min-width:768px){.user-card-stats{margin-top:20px}}.user-card-stats ul{margin:0}.user-card-stats li{display:inline-block;margin-right:12px;color:#777;font-size:12px}.user-card-stats li.user-stat-empty{display:none}@media screen and (min-width:768px){.user-card-stats li{margin:0 6px}li.user-stat-divider{display:block;margin:0}.user-card-stats{min-height:60px}}@media screen and (max-width:767px){li.user-stat-divider{display:none}.user-card-left{padding-right:0}}.progress,.progress .progress-bar{-webkit-box-shadow:none;box-shadow:none;height:8px}.page-user-profile .page-header img,.well{box-shadow:0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.misago-markup .quote-block,.misago-markup blockquote{background-color:#ffecb3;border-color:#b3e5fc;overflow:hidden;color:#3e2723}.misago-markup .quote-block .quote-heading,.misago-markup blockquote .quote-heading{background-color:#ffe082;border:none;color:#795548}.misago-markup .quote-block .quote-heading a,.misago-markup .quote-block .quote-heading a:link,.misago-markup .quote-block .quote-heading a:visited,.misago-markup blockquote .quote-heading a,.misago-markup blockquote .quote-heading a:link,.misago-markup blockquote .quote-heading a:visited{color:#5d4037}.misago-markup .quote-block .quote-heading a:active,.misago-markup .quote-block .quote-heading a:focus,.misago-markup .quote-block .quote-heading a:hover,.misago-markup blockquote .quote-heading a:active,.misago-markup blockquote .quote-heading a:focus,.misago-markup blockquote .quote-heading a:hover{color:#3e2723}.misago-markup .quote-block hr,.misago-markup blockquote hr{border-color:#ffca28}.misago-markup>.quote-block,.misago-markup>blockquote{background:#ffecb3;border:none;border-radius:4px}.misago-markup .quote-body,.misago-markup>.quote-block .quote-block,.misago-markup>blockquote .quote-block{background:#ffecb3}.misago-markup .quote-body>.quote-block,.misago-markup .quote-body>blockquote{border-color:#ffe082;border-radius:4px}.modal-header{background:#eee;border-bottom-color:#eee;border-radius:6px 6px 0 0;color:#424242}.modal-header .close{padding:0 6px;color:#424242;font-size:24px;line-height:24px;text-shadow:none}.modal-message .message-icon{color:#9575cd}.modal-body>.form-group{margin:20px 0}.modal-body>.form-group:first-child{margin-top:0}.modal-body>.form-group:last-child{margin-bottom:0}.modal-sign-in .modal-body{padding-top:0;padding-bottom:0}.modal-sign-in .modal-body>.form-group{margin:20px 0}.legal-footnote label{font-weight:700}.legal-footnote .help-block{margin-left:20px;color:#f44336}.modal-avatar-index .avatar-preview{background:#fff;border-radius:6px}.modal-avatar-crop .cropit-preview{background:#fff;margin:20px 0}.modal-avatar-crop .cropit-image-zoom-input{margin-top:40px;margin-bottom:20px}.modal-post-likers .media{border-bottom:1px solid #eee;padding-bottom:15px}.modal-post-likers .media img{border-radius:4px;width:40px;height:40px}.modal-post-likers .media:last-child{border:none;padding-bottom:0}.well{border:1px solid #ddd}.active-posters .rank-user-avatar img{border-radius:4px}.active-posters .user-details{color:#777}.active-posters .user-details a.rank-name,.active-posters .user-details a.rank-name:link,.active-posters .user-details a.rank-name:visited{color:#555;font-weight:400}.active-posters .user-details a.rank-name:active,.active-posters .user-details a.rank-name:focus,.active-posters .user-details a.rank-name:hover{color:#212121}.active-posters .user-details span.rank-name{color:#555;font-weight:400}.active-posters small{color:#777}.page-error .message-icon{color:#ef5350}.page-message .message-icon,.page-message-info .message-icon{color:#9575cd}.page-message-success .message-icon{color:#81c784}.page-options .message-line,.page-options .message-line a,.page-options .message-line a:link,.page-options .message-line a:visited{color:#777}.page-options .message-line a:active,.page-options .message-line a:focus,.page-options .message-line a:focus:active,.page-options .message-line a:hover{color:#212121}.page-user-profile .page-header img{background-color:#fff;border:3px solid #fff;border-radius:6px}.page-user-profile .page-header .user-status .status-icon{width:14px;height:14px;font-size:14px;line-height:14px}.page-user-profile .profile-side-avatar img{background-color:#fff;border:3px solid #fff;border-radius:6px;box-shadow:0 4px 5px 0 rgba(0,0,0,.04),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.06)}.panel-profile-details-group.panel{border:1px solid #ddd}.panel-profile-details-group.panel .form-group{margin:0}.panel-profile-details-group .form-control-static p:last-child{margin-bottom:0}.list-group-item.list-group-category-has-flavor{border-left:5px solid #fff;padding-left:10px}.list-group-item.list-group-item-category-red{border-left-color:#f44336}.list-group-item.list-group-item-category-light-red{border-left-color:#ef9a9a}.list-group-item.list-group-item-category-pink{border-left-color:#e91e63}.list-group-item.list-group-item-category-light-pink{border-left-color:#f48fb1}.list-group-item.list-group-item-category-purple{border-left-color:#9c27b0}.list-group-item.list-group-item-category-light-purple{border-left-color:#ce93d8}.list-group-item.list-group-item-category-deep-purple{border-left-color:#673ab7}.list-group-item.list-group-item-category-indigo{border-left-color:#3f51b5}.list-group-item.list-group-item-category-light-indigo{border-left-color:#9fa8da}.list-group-item.list-group-item-category-blue{border-left-color:#2196f3}.list-group-item.list-group-item-category-light-blue{border-left-color:#81d4fa}.list-group-item.list-group-item-category-cyan{border-left-color:#00bcd4}.list-group-item.list-group-item-category-light-cyan{border-left-color:#80deea}.list-group-item.list-group-item-category-teal{border-left-color:#009688}.list-group-item.list-group-item-category-light-teal{border-left-color:#80cbc4}.list-group-item.list-group-item-category-green{border-left-color:#4caf50}.list-group-item.list-group-item-category-light-green{border-left-color:#aed581}.list-group-item.list-group-item-category-lime{border-left-color:#cddc39}.list-group-item.list-group-item-category-yellow{border-left-color:#ffeb3b}.list-group-item.list-group-item-category-amber{border-left-color:#ffc107}.list-group-item.list-group-item-category-orange{border-left-color:#ff9800}.list-group-item.list-group-item-category-deep-orange{border-left-color:#ff5722}.list-group-item.list-group-item-category-brown{border-left-color:#795548}.list-group-item.list-group-item-category-light-brown{border-left-color:#bcaaa4}.list-group-item.list-group-item-category-blue-grey{border-left-color:#607d8b}.list-group-item.list-group-item-category-light-blue-grey{border-left-color:#b0bec5}.list-group-item.list-group-item-category-grey{border-left-color:#9e9e9e}.list-group-item.list-group-item-category-black{border-left-color:#000}.post-primary .panel-post{border-color:#b388ff}.post-primary .user-title,.post-primary .user-title a,.post-primary .user-title a:active,.post-primary .user-title a:focus,.post-primary .user-title a:hover,.post-primary .user-title a:link,.post-primary .user-title a:visited{color:#6200ea}.post-success .panel-post{border-color:#00c853}.post-success .user-title,.post-success .user-title a,.post-success .user-title a:active,.post-success .user-title a:focus,.post-success .user-title a:hover,.post-success .user-title a:link,.post-success .user-title a:visited{color:#388e3c}.post-warning .panel-post{border-color:#ffab40}.post-warning .user-title,.post-warning .user-title a,.post-warning .user-title a:active,.post-warning .user-title a:focus,.post-warning .user-title a:hover,.post-warning .user-title a:link,.post-warning .user-title a:visited{color:#ff6d00}.post-danger .panel-post{border-color:#ff8a80}.post-danger .user-title,.post-danger .user-title a,.post-danger .user-title a:active,.post-danger .user-title a:focus,.post-danger .user-title a:hover,.post-danger .user-title a:link,.post-danger .user-title a:visited{color:#d50000}.user-card-primary .panel-body{background:#fff;border:2px solid #b388ff;border-radius:4px;padding:13px}.user-card-primary .user-card-title,.user-card-primary .user-card-title a.user-title,.user-card-primary .user-card-title a.user-title:active,.user-card-primary .user-card-title a.user-title:focus,.user-card-primary .user-card-title a.user-title:hover,.user-card-primary .user-card-title a.user-title:link,.user-card-primary .user-card-title a.user-title:visited{color:#6200ea}.user-card-primary .user-card-stats li{color:#9575cd}.user-card-success .panel-body{background:#fff;border:2px solid #00c853;border-radius:4px;padding:13px}.user-card-success .user-card-title,.user-card-success .user-card-title a.user-title,.user-card-success .user-card-title a.user-title:active,.user-card-success .user-card-title a.user-title:focus,.user-card-success .user-card-title a.user-title:hover,.user-card-success .user-card-title a.user-title:link,.user-card-success .user-card-title a.user-title:visited{color:#388e3c}.user-card-success .user-card-stats li{color:#66bb6a}.user-card-warning .panel-body{background:#fff;border:2px solid #ffab40;border-radius:4px;padding:13px}.user-card-warning .user-card-title,.user-card-warning .user-card-title a.user-title,.user-card-warning .user-card-title a.user-title:active,.user-card-warning .user-card-title a.user-title:focus,.user-card-warning .user-card-title a.user-title:hover,.user-card-warning .user-card-title a.user-title:link,.user-card-warning .user-card-title a.user-title:visited{color:#ff6d00}.user-card-warning .user-card-stats li{color:#ff6e40}.user-card-danger .panel-body{background:#fff;border:2px solid #ff8a80;border-radius:4px;padding:13px}.user-card-danger .user-card-title,.user-card-danger .user-card-title a.user-title,.user-card-danger .user-card-title a.user-title:active,.user-card-danger .user-card-title a.user-title:focus,.user-card-danger .user-card-title a.user-title:hover,.user-card-danger .user-card-title a.user-title:link,.user-card-danger .user-card-title a.user-title:visited{color:#d50000}.user-card-danger .user-card-stats li{color:#e57373}.list-group .list-group-rank-primary{border-left:4px solid #7e57c2;padding-left:11px}.list-group .list-group-rank-primary a.rank-name,.list-group .list-group-rank-primary a.rank-name:active,.list-group .list-group-rank-primary a.rank-name:focus,.list-group .list-group-rank-primary a.rank-name:hover,.list-group .list-group-rank-primary a.rank-name:link,.list-group .list-group-rank-primary a.rank-name:visited,.list-group .list-group-rank-primary span.rank-name{color:#6200ea}.list-group .list-group-rank-success{border-left:4px solid #9ccc65;padding-left:11px}.list-group .list-group-rank-success a.rank-name,.list-group .list-group-rank-success a.rank-name:active,.list-group .list-group-rank-success a.rank-name:focus,.list-group .list-group-rank-success a.rank-name:hover,.list-group .list-group-rank-success a.rank-name:link,.list-group .list-group-rank-success a.rank-name:visited,.list-group .list-group-rank-success span.rank-name{color:#388e3c}.list-group .list-group-rank-warning{border-left:4px solid #ff7043;padding-left:11px}.list-group .list-group-rank-warning a.rank-name,.list-group .list-group-rank-warning a.rank-name:active,.list-group .list-group-rank-warning a.rank-name:focus,.list-group .list-group-rank-warning a.rank-name:hover,.list-group .list-group-rank-warning a.rank-name:link,.list-group .list-group-rank-warning a.rank-name:visited,.list-group .list-group-rank-warning span.rank-name{color:#ff6d00}.list-group .list-group-rank-danger{border-left:4px solid #f44336;padding-left:11px}.list-group .list-group-rank-danger a.rank-name,.list-group .list-group-rank-danger a.rank-name:active,.list-group .list-group-rank-danger a.rank-name:focus,.list-group .list-group-rank-danger a.rank-name:hover,.list-group .list-group-rank-danger a.rank-name:link,.list-group .list-group-rank-danger a.rank-name:visited,.list-group .list-group-rank-danger span.rank-name{color:#d50000}.page-header-rank-primary .user-rank,.page-header-rank-primary .user-rank a,.page-header-rank-primary .user-rank a:active,.page-header-rank-primary .user-rank a:focus,.page-header-rank-primary .user-rank a:hover,.page-header-rank-primary .user-rank a:link,.page-header-rank-primary .user-rank a:visited{color:#6200ea}.page-header-rank-success .user-rank,.page-header-rank-success .user-rank a,.page-header-rank-success .user-rank a:active,.page-header-rank-success .user-rank a:focus,.page-header-rank-success .user-rank a:hover,.page-header-rank-success .user-rank a:link,.page-header-rank-success .user-rank a:visited{color:#388e3c}.page-header-rank-warning .user-rank,.page-header-rank-warning .user-rank a,.page-header-rank-warning .user-rank a:active,.page-header-rank-warning .user-rank a:focus,.page-header-rank-warning .user-rank a:hover,.page-header-rank-warning .user-rank a:link,.page-header-rank-warning .user-rank a:visited{color:#ff6d00}.page-header-rank-danger .user-rank,.page-header-rank-danger .user-rank a,.page-header-rank-danger .user-rank a:active,.page-header-rank-danger .user-rank a:focus,.page-header-rank-danger .user-rank a:hover,.page-header-rank-danger .user-rank a:link,.page-header-rank-danger .user-rank a:visited{color:#d50000}

+ 25 - 24
misago/static/misago/js/misago.js

@@ -1,25 +1,26 @@
-!function e(t,a,n){function r(l,s){if(!a[l]){if(!t[l]){var i="function"==typeof require&&require;if(!s&&i)return i(l,!0);if(o)return o(l,!0);var u=new Error("Cannot find module '"+l+"'");throw u.code="MODULE_NOT_FOUND",u}var c=a[l]={exports:{}};t[l][0].call(c.exports,function(e){var a=t[l][1][e];return r(a?a:e)},c,c.exports,e,t,a,n)}return a[l].exports}for(var o="function"==typeof require&&require,l=0;l<n.length;l++)r(n[l]);return r}({1:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e(".."),s=n(l),i=function(){return s["default"].get("TERMS_OF_SERVICE_URL")?o["default"].createElement("p",{className:"legal-footnote"},o["default"].createElement("span",{className:"material-icon"},"info_outline"),o["default"].createElement("a",{href:s["default"].get("TERMS_OF_SERVICE_URL"),target:"_blank"},gettext("By registering you agree to site's terms and conditions."))):null};a["default"]=i},{"..":299,react:"react"}],2:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e(".."),s=n(l),i=function(e){var t=e.buttonClassName,a=e.buttonLabel,n=e.formLabel,r=e.header,l=e.labelClassName,i=s["default"].get("SETTINGS").SOCIAL_AUTH;return 0===i.length?null:o["default"].createElement("div",{className:"form-group form-social-auth"},o["default"].createElement(u,{className:l,text:r}),o["default"].createElement("div",{className:"row"},i.map(function(e){var n=e.id,r=e.name,l=e.url,s="btn btn-block btn-default btn-social-"+n,i=interpolate(a,{site:r},!0);return o["default"].createElement("div",{className:t||"col-xs-12",key:n},o["default"].createElement("a",{className:s,href:l},i))})),o["default"].createElement("hr",null),o["default"].createElement(u,{className:l,text:n}))},u=function(e){var t=e.className,a=e.text;return a?o["default"].createElement("h5",{className:t||""},a):null};a["default"]=i},{"..":299,react:"react"}],3:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Add participant")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("./form"),p=r(f),m=e("./form-group"),h=r(m),b=e("../reducers/participants"),v=n(b),_=e("../reducers/thread"),g=e("../services/ajax"),y=r(g),E=e("../services/modal"),w=r(E),O=e("../services/snackbar"),k=r(O),N=e("../services/store"),x=r(N),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUsernameChange=function(e){a.changeValue("username",e.target.value)},a.state={isLoading:!1,username:""},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.username.trim().length||(k["default"].error(gettext("You have to enter user name.")),!1)}},{key:"send",value:function(){return y["default"].patch(this.props.thread.api.index,[{op:"add",path:"participants",value:this.state.username},{op:"add",path:"acl",value:1}])}},{key:"handleSuccess",value:function(e){x["default"].dispatch((0,_.updateAcl)(e)),x["default"].dispatch(v.replace(e.participants)),k["default"].success(gettext("New participant has been added to thread.")),w["default"].hide()}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog modal-sm",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(h["default"],{"for":"id_username",label:gettext("User to add")},d["default"].createElement("input",{id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.onUsernameChange,type:"text",value:this.state.username}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-block btn-primary",disabled:this.state.isLoading},gettext("Add participant")),d["default"].createElement("button",{className:"btn btn-block btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel"))))))}}]),t}(p["default"]);a["default"]=j},{"../reducers/participants":347,"../reducers/thread":356,"../services/ajax":361,"../services/modal":367,"../services/snackbar":372,"../services/store":373,"./form":54,"./form-group":53,react:"react"}],4:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{user:e.auth.user,signedIn:e.auth.signedIn,signedOut:e.auth.signedOut}}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"refresh",value:function(){window.location.reload()}},{key:"getMessage",value:function(){return this.props.signedIn?interpolate(gettext("You have signed in as %(username)s. Please refresh the page before continuing."),{username:this.props.signedIn.username},!0):this.props.signedOut?interpolate(gettext("%(username)s, you have been signed out. Please refresh the page before continuing."),{username:this.props.user.username},!0):void 0}},{key:"render",value:function(){var e="auth-message";return(this.props.signedIn||this.props.signedOut)&&(e+=" show"),c["default"].createElement("div",{className:e},c["default"].createElement("div",{className:"container"},c["default"].createElement("p",{className:"lead"},this.getMessage()),c["default"].createElement("p",null,c["default"].createElement("button",{className:"btn btn-default",type:"button",onClick:this.refresh},gettext("Reload page")),c["default"].createElement("span",{className:"hidden-xs hidden-sm"}," "+gettext("or press F5 key.")))))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],5:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){return e&&e.id?o(e.avatars,t).url:u["default"].get("BLANK_AVATAR_URL")}function o(e,t){var a=e[0];return e.forEach(function(e){e.size>=t&&(a=e)}),a}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.size||100,a=e.size2x||t;return s["default"].createElement("img",{alt:"",className:e.className||"user-avatar",src:r(e.user,t),srcSet:r(e.user,a),width:t,height:t})},a.getSrc=r,a.resolveAvatarForSize=o;var l=e("react"),s=n(l),i=e(".."),u=n(i)},{"..":299,react:"react"}],6:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getReasonMessage",value:function(){return this.props.message.html?d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.props.message.html}}):d["default"].createElement("p",{className:"lead"},this.props.message.plain)}},{key:"getExpirationMessage",value:function(){if(this.props.expires){if(this.props.expires.isAfter((0,u["default"])())){var e=interpolate(gettext("This ban expires on %(expires_on)s."),{expires_on:this.props.expires.format("LL, LT")},!0),t=interpolate(gettext("This ban expires %(expires_on)s."),{expires_on:this.props.expires.fromNow()},!0);return d["default"].createElement("abbr",{title:e},t)}return gettext("This ban has expired.")}return gettext("This ban is permanent.")}},{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-error page-error-banned"},d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"message-panel"},d["default"].createElement("div",{className:"message-icon"},d["default"].createElement("span",{className:"material-icon"},"highlight_off")),d["default"].createElement("div",{className:"message-body"},this.getReasonMessage(),d["default"].createElement("p",{className:"message-footnote"},this.getExpirationMessage())))))}}]),t}(d["default"].Component);a["default"]=f},{moment:"moment",react:"react"}],7:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){var e="btn "+this.props.className,t=this.props.disabled;return this.props.loading&&(e+=" btn-loading",t=!0),u["default"].createElement("button",{className:e,disabled:t,onClick:this.props.onClick,type:this.props.onClick?"button":"submit"},this.props.children,this.props.loading?u["default"].createElement(d["default"],null):null)}}]),t}(u["default"].Component);a["default"]=f,f.defaultProps={className:"btn-default",type:"submit",loading:!1,disabled:!1,onClick:null}},{"./loader":56,react:"react"}],8:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"categories-list"},o["default"].createElement("ul",{className:"list-group"},o["default"].createElement("li",{className:"list-group-item empty-message"},o["default"].createElement("p",{className:"lead"},gettext("No categories exist or you don't have permission to see them.")))))};var r=e("react"),o=n(r)},{react:"react"}],9:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.categories;return o["default"].createElement("div",{className:"categories-list"},t.map(function(e){return o["default"].createElement(s["default"],{category:e,key:e.id})}))};var r=e("react"),o=n(r),l=e("./category"),s=n(l)},{"./category":10,react:"react"}],10:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a="list-group list-group-category";return t.css_class&&(a+=" list-group-category-has-flavor",a+=" list-group-category-"+t.css_class),o["default"].createElement("ul",{className:a},o["default"].createElement(s["default"],{category:t,isFirst:!0}),t.subcategories.map(function(e){return o["default"].createElement(s["default"],{category:e,isFirst:!1,key:e.id})}))};var r=e("react"),o=n(r),l=e("./list-item"),s=n(l)},{"./list-item":13,react:"react"}],11:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return t.description?o["default"].createElement("div",{className:"category-description",dangerouslySetInnerHTML:{__html:t.description.html}}):null};var r=e("react"),o=n(r)},{react:"react"}],12:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.is_read?"read-status item-read":"read-status item-new"}function o(e){return e.is_closed?e.is_read?gettext("This category has no new posts. (closed)"):gettext("This category has new posts. (closed)"):e.is_read?gettext("This category has no new posts."):gettext("This category has new posts.")}function l(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return i["default"].createElement("div",{className:r(t),title:o(t)},i["default"].createElement("span",{className:"material-icon"},l(t)))},a.getClassName=r,a.getTitle=o,a.getIcon=l;var s=e("react"),i=n(s)},{react:"react"}],13:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isFirst,n="list-group-item";return n+=t.description?" list-group-category-has-description":" list-group-category-no-description",a&&(n+=" list-group-item-first"),t.css_class&&(n+=" list-group-category-has-flavor",n+=" list-group-item-category-"+t.css_class),o["default"].createElement("li",{className:n},o["default"].createElement("div",{className:"row"},o["default"].createElement(s["default"],{category:t}),o["default"].createElement(d["default"],{category:t}),o["default"].createElement(u["default"],{category:t})),o["default"].createElement(p["default"],{category:t,isFirst:a}))};var r=e("react"),o=n(r),l=e("./main"),s=n(l),i=e("./last-thread"),u=n(i),c=e("./stats"),d=n(c),f=e("./subcategories"),p=n(f)},{"./last-thread":14,"./main":15,"./stats":16,"./subcategories":17,react:"react"}],14:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.category;return t.acl.can_browse&&t.acl.can_see_all_threads&&t.last_thread_title?f["default"].createElement("div",{className:"media"},f["default"].createElement("div",{className:"media-left hidden-xs"},f["default"].createElement(o,{category:t})),f["default"].createElement("div",{className:"media-body"},f["default"].createElement("div",{className:"media-heading"},f["default"].createElement("a",{className:"item-title thread-title",href:t.url.last_thread_new,title:t.last_thread_title},t.last_thread_title)),f["default"].createElement("ul",{className:"list-inline"},f["default"].createElement("li",{className:"category-last-thread-poster"},f["default"].createElement(l,{category:t})),f["default"].createElement("li",{className:"divider"},"—"),f["default"].createElement("li",{className:"category-last-thread-date"},f["default"].createElement("a",{href:t.url.last_post},t.last_post_on.fromNow()))))):null}function o(e){var t=e.category;return t.last_poster?f["default"].createElement("a",{className:"last-poster-avatar",href:t.last_poster.url,title:t.last_poster_name},f["default"].createElement(m["default"],{className:"media-object",size:40,user:t.last_poster})):f["default"].createElement("span",{className:"last-poster-avatar",title:t.last_poster_name},f["default"].createElement(m["default"],{className:"media-object",size:40}))}function l(e){var t=e.category;return t.last_poster?f["default"].createElement("a",{className:"item-title",href:t.last_poster.url},t.last_poster_name):f["default"].createElement("span",{className:"item-title"},t.last_poster_name)}function s(e){var t=e.category;return t.acl.can_browse&&t.acl.can_see_all_threads?t.last_thread_title?null:f["default"].createElement(c,{message:gettext("This category is empty. No threads were posted within it so far.")}):null}function i(e){var t=e.category;return t.acl.can_browse?t.acl.can_see_all_threads?null:f["default"].createElement(c,{message:gettext("This category is private. You can see only your own threads within it.")}):null}function u(e){var t=e.category;return t.acl.can_browse?null:f["default"].createElement(c,{message:gettext("This category is protected. You can't browse it's contents.")})}function c(e){var t=e.message;return f["default"].createElement("div",{className:"media category-thread-message"},f["default"].createElement("div",{className:"media-left"},f["default"].createElement("span",{className:"material-icon"},"info_outline")),f["default"].createElement("div",{className:"media-body"},f["default"].createElement("p",null,t)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return f["default"].createElement("div",{className:"col-xs-12 col-sm-6 col-md-4 category-last-thread"},f["default"].createElement(r,{category:t}),f["default"].createElement(s,{category:t}),f["default"].createElement(i,{category:t}),f["default"].createElement(u,{category:t}))},a.LastThread=r,a.LastPosterAvatar=o,a.LastPosterName=l,a.Empty=s,a.Private=i,a.Protected=u,a.Message=c;var d=e("react"),f=n(d),p=e("../../../avatar"),m=n(p)},{"../../../avatar":5,react:"react"}],15:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return o["default"].createElement("div",{className:"col-xs-12 col-sm-6 col-md-6 category-main"},o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement(u["default"],{category:t})),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("h4",{className:"media-heading"},o["default"].createElement("a",{href:t.url.index},t.name)),o["default"].createElement(s["default"],{category:t}))))};var r=e("react"),o=n(r),l=e("./description"),s=n(l),i=e("./icon"),u=n(i)},{"./description":11,"./icon":12,react:"react"}],16:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.threads,a=ngettext("%(threads)s thread","%(threads)s threads",t);return s["default"].createElement("li",{className:"category-stat-threads"},interpolate(a,{threads:t},!0))}function o(e){var t=e.posts,a=ngettext("%(posts)s post","%(posts)s posts",t);return s["default"].createElement("li",{className:"category-stat-posts"},interpolate(a,{posts:t},!0))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return s["default"].createElement("div",{className:"col-md-2 hidden-xs hidden-sm"},s["default"].createElement("ul",{className:"list-unstyled category-stats"},s["default"].createElement(r,{threads:t.threads}),s["default"].createElement(o,{posts:t.posts})))},a.Threads=r,a.Posts=o;var l=e("react"),s=n(l),i=e("../../../avatar");n(i)},{"../../../avatar":5,react:"react"}],17:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isFirst;return a?null:0===t.subcategories.length?null:o["default"].createElement("div",{className:"row subcategories-list"},t.subcategories.map(function(e){return o["default"].createElement(s["default"],{category:e,key:e.id})}))};var r=e("react"),o=n(r),l=e("./list-item"),s=n(l)},{"./list-item":18,react:"react"}],18:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a="btn btn-default btn-block btn-sm btn-subcategory";return t.is_read||(a+=" btn-subcategory-new"),l["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3"},l["default"].createElement("a",{className:a,href:t.url.index},l["default"].createElement("span",{className:"material-icon"},r(t)),l["default"].createElement("span",{className:"icon-text"},t.name)))},a.getIcon=r;var o=e("react"),l=n(o)},{react:"react"}],19:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick}}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("moment"),c=n(u),d=e("react"),f=n(d),p=e("./blankslate"),m=n(p),h=e("./categories-list"),b=n(h),v=e("../../index"),_=n(v),g=e("../../services/polls"),y=n(g),E=function O(e){return Object.assign({},e,{last_post_on:e.last_post_on?(0,c["default"])(e.last_post_on):null,subcategories:e.subcategories.map(O)})},w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){a.setState({categories:e.map(E)})},a.state={categories:_["default"].get("CATEGORIES").map(E)},a.startPolling(_["default"].get("CATEGORIES_API")),a}return l(t,e),i(t,[{key:"startPolling",value:function(e){y["default"].start({poll:"categories",url:e,frequency:18e4,update:this.update})}},{key:"render",value:function(){var e=this.state.categories;return 0===e.length?f["default"].createElement(m["default"],null):f["default"].createElement(b["default"],{categories:e})}}]),t}(f["default"].Component);a["default"]=w},{"../../index":299,"../../services/polls":370,"./blankslate":8,"./categories-list":9,moment:"moment",react:"react"}],20:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("select",{className:e.className||"form-control",disabled:e.disabled||!1,id:e.id||null,onChange:e.onChange,value:e.value},e.choices.map(function(e){return o["default"].createElement("option",{disabled:e.disabled||!1,key:e.value,value:e.value},"- - ".repeat(e.level)+e.label)}))};var r=e("react"),o=n(r)},{react:"react"}],21:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=(n(c),e("../button")),f=n(d),p=e("../../services/ajax"),m=n(p),h=e("../../services/snackbar"),b=n(h),v=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.cropAvatar=function(){if(a.state.isLoading)return!1;a.setState({isLoading:!0});var e=a.props.upload?"crop_tmp":"crop_src",t=$(".crop-form"),n=t.cropit("exportZoom"),r=t.cropit("offset");m["default"].post(a.props.user.api.avatar,{avatar:e,crop:{offset:{x:r.x*n,y:r.y*n},zoom:t.cropit("zoom")*n}}).then(function(e){a.props.onComplete(e),b["default"].success(e.detail)},function(e){400===e.status?(b["default"].error(e.detail),a.setState({isLoading:!1})):a.props.showError(e)})},a.state={isLoading:!1,deviceRatio:1},a}return l(t,e),s(t,[{key:"getAvatarSize",value:function(){return this.props.upload?this.props.options.crop_tmp.size:this.props.options.crop_src.size}},{key:"getImagePath",value:function(){return this.props.upload?this.props.dataUrl:this.props.options.crop_src.url}},{key:"componentDidMount",value:function(){for(var e=this,t=$(".crop-form"),a=this.getAvatarSize(),n=t.width();n<a;)a/=2;var r=this.getAvatarSize()/a;t.width(a),t.cropit({width:a,height:a,exportZoom:r,imageState:{src:this.getImagePath()},onImageLoaded:function(){if(e.props.upload){var a=t.cropit("zoom"),n=t.cropit("imageSize");if(n.width>n.height){var r=n.width*a,o=(r-e.getAvatarSize())/-2;t.cropit("offset",{x:o,y:0})}else if(n.width<n.height){var l=n.height*a,s=(l-e.getAvatarSize())/-2;t.cropit("offset",{x:0,y:s})}else t.cropit("offset",{x:0,y:0})}else{var i=e.props.options.crop_src.crop;i&&(t.cropit("zoom",i.zoom),t.cropit("offset",{x:i.x,y:i.y}))}}})}},{key:"componentWillUnmount",value:function(){$(".crop-form").cropit("disable")}},{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement("div",{className:"modal-body modal-avatar-crop"},u["default"].createElement("div",{className:"crop-form"},u["default"].createElement("div",{className:"cropit-preview"}),u["default"].createElement("input",{type:"range",className:"cropit-image-zoom-input"}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},u["default"].createElement(f["default"],{onClick:this.cropAvatar,loading:this.state.isLoading,className:"btn-primary btn-block"},this.props.upload?gettext("Set avatar"):gettext("Crop image")),u["default"].createElement(f["default"],{onClick:this.props.showIndex,disabled:this.state.isLoading,className:"btn-default btn-block"},gettext("Cancel")))))}}]),t}(u["default"].Component);a["default"]=v},{"../../services/ajax":361,"../../services/snackbar":372,"../avatar":5,"../button":7,react:"react"}],22:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Gallery=a.GalleryItem=void 0;var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../avatar"),f=(n(d),e("../button")),p=n(f),m=e("../../index"),h=(n(m),e("../../services/ajax")),b=n(h),v=e("../../services/snackbar"),_=n(v),g=e("../../utils/batch"),y=n(g),E=a.GalleryItem=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.select=function(){n.props.select(n.props.id)},l=a,o(n,l)}return l(t,e),i(t,[{key:"getClassName",value:function(){return this.props.selection===this.props.id?this.props.disabled?"btn btn-avatar btn-disabled avatar-selected":"btn btn-avatar avatar-selected":this.props.disabled?"btn btn-avatar btn-disabled":"btn btn-avatar"}},{key:"render",value:function(){return c["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,onClick:this.select},c["default"].createElement("img",{src:this.props.url}))}}]),t}(c["default"].Component),w=a.Gallery=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"avatars-gallery"},c["default"].createElement("h3",null,this.props.name),c["default"].createElement("div",{className:"avatars-gallery-images"},(0,y["default"])(this.props.images,4,null).map(function(t,a){return c["default"].createElement("div",{className:"row",key:a},t.map(function(t,a){return c["default"].createElement("div",{className:"col-xs-3",key:a},t?c["default"].createElement(E,s({disabled:e.props.disabled,select:e.props.select,selection:e.props.selection},t)):c["default"].createElement("div",{className:"blank-avatar"}))}))})))}}]),t}(c["default"].Component),O=function(e){function t(e){r(this,t);
-var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.select=function(e){a.setState({selection:e})},a.save=function(){return!a.state.isLoading&&(a.setState({isLoading:!0}),void b["default"].post(a.props.user.api.avatar,{avatar:"galleries",image:a.state.selection}).then(function(e){a.setState({isLoading:!1}),_["default"].success(e.detail),a.props.onComplete(e),a.props.showIndex()},function(e){400===e.status?(_["default"].error(e.detail),a.setState({isLoading:!1})):a.props.showError(e)}))},a.state={selection:null,isLoading:!1},a}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",null,c["default"].createElement("div",{className:"modal-body modal-avatar-gallery"},this.props.options.galleries.map(function(t,a){return c["default"].createElement(w,{name:t.name,images:t.images,selection:e.state.selection,disabled:e.state.isLoading,select:e.select,key:a})})),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},c["default"].createElement(p["default"],{onClick:this.save,loading:this.state.isLoading,disabled:!this.state.selection,className:"btn-primary btn-block"},this.state.selection?gettext("Save choice"):gettext("Select avatar")),c["default"].createElement(p["default"],{onClick:this.props.showIndex,disabled:this.state.isLoading,className:"btn-default btn-block"},gettext("Cancel"))))))}}]),t}(c["default"].Component);a["default"]=O},{"../../index":299,"../../services/ajax":361,"../../services/snackbar":372,"../../utils/batch":376,"../avatar":5,"../button":7,react:"react"}],23:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=e("../button"),p=n(f),m=e("../loader"),h=n(m),b=e("../../services/ajax"),v=n(b),_=e("../../services/snackbar"),g=n(_),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.setGravatar=function(){a.callApi("gravatar")},a.setGenerated=function(){a.callApi("generated")},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"callApi",value:function(e){var t=this;return!this.state.isLoading&&(this.setState({isLoading:!0}),void v["default"].post(this.props.user.api.avatar,{avatar:e}).then(function(e){t.setState({isLoading:!1}),g["default"].success(e.detail),t.props.onComplete(e)},function(e){400===e.status?(g["default"].error(e.detail),t.setState({isLoading:!1})):t.props.showError(e)}))}},{key:"getGravatarButton",value:function(){return this.props.options.gravatar?u["default"].createElement(p["default"],{onClick:this.setGravatar,disabled:this.state.isLoading,className:"btn-default btn-block btn-avatar-gravatar"},gettext("Download my Gravatar")):null}},{key:"getCropButton",value:function(){return this.props.options.crop_src?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-crop",disabled:this.state.isLoading,onClick:this.props.showCrop},gettext("Re-crop uploaded image")):null}},{key:"getUploadButton",value:function(){return this.props.options.upload?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-upload",disabled:this.state.isLoading,onClick:this.props.showUpload},gettext("Upload new image")):null}},{key:"getGalleryButton",value:function(){return this.props.options.galleries?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-gallery",disabled:this.state.isLoading,onClick:this.props.showGallery},gettext("Pick avatar from gallery")):null}},{key:"getAvatarPreview",value:function(){var e={id:this.props.user.id,avatars:this.props.options.avatars};return this.state.isLoading?u["default"].createElement("div",{className:"avatar-preview preview-loading"},u["default"].createElement(d["default"],{size:"200",user:e}),u["default"].createElement(h["default"],null)):u["default"].createElement("div",{className:"avatar-preview"},u["default"].createElement(d["default"],{size:"200",user:e}))}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-index"},u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-md-5"},this.getAvatarPreview()),u["default"].createElement("div",{className:"col-md-7"},this.getGravatarButton(),u["default"].createElement(p["default"],{onClick:this.setGenerated,disabled:this.state.isLoading,className:"btn-default btn-block btn-avatar-generate"},gettext("Generate my individual avatar")),this.getCropButton(),this.getUploadButton(),this.getGalleryButton())))}}]),t}(u["default"].Component);a["default"]=y},{"../../services/ajax":361,"../../services/snackbar":372,"../avatar":5,"../button":7,"../loader":56,react:"react"}],24:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{user:e.auth.user}}Object.defineProperty(a,"__esModule",{value:!0}),a.ChangeAvatarError=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=e("./index"),f=n(d),p=e("./crop"),m=n(p),h=e("./upload"),b=n(h),v=e("./gallery"),_=n(v),g=e("../modal-loader"),y=n(g),E=e("../../reducers/users"),w=e("../../services/ajax"),O=n(w),k=e("../../services/store"),N=n(k),x=a.ChangeAvatarError=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getErrorReason",value:function(){return this.props.reason?c["default"].createElement("p",{dangerouslySetInnerHTML:{__html:this.props.reason}}):null}},{key:"render",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"remove_circle_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},this.props.message),this.getErrorReason(),c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}}]),t}(c["default"].Component),j=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showError=function(e){n.setState({error:e})},n.showIndex=function(){n.setState({component:f["default"]})},n.showUpload=function(){n.setState({component:b["default"]})},n.showCrop=function(){n.setState({component:m["default"]})},n.showGallery=function(){n.setState({component:_["default"]})},n.completeFlow=function(e){N["default"].dispatch((0,E.updateAvatar)(n.props.user,e.avatars)),n.setState({component:f["default"],options:e})},l=a,o(n,l)}return l(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;O["default"].get(this.props.user.api.avatar).then(function(t){e.setState({component:f["default"],options:t,error:null})},function(t){e.showError(t)})}},{key:"getBody",value:function(){return this.state?this.state.error?c["default"].createElement(x,{message:this.state.error.detail,reason:this.state.error.reason}):c["default"].createElement(this.state.component,{options:this.state.options,user:this.props.user,onComplete:this.completeFlow,showError:this.showError,showIndex:this.showIndex,showCrop:this.showCrop,showUpload:this.showUpload,showGallery:this.showGallery}):c["default"].createElement(y["default"],null)}},{key:"getClassName",value:function(){return this.state&&this.state.error?"modal-dialog modal-message modal-change-avatar":"modal-dialog modal-change-avatar"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Change your avatar"))),this.getBody()))}}]),t}(c["default"].Component);a["default"]=j},{"../../reducers/users":360,"../../services/ajax":361,"../../services/store":373,"../modal-loader":59,"./crop":21,"./gallery":22,"./index":23,"./upload":25,react:"react"}],25:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./crop"),d=n(c),f=e("../button"),p=n(f),m=e("../../services/ajax"),h=n(m),b=e("../../services/snackbar"),v=n(b),_=e("../../utils/file-size"),g=n(_),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.pickFile=function(){document.getElementById("avatar-hidden-upload").click()},a.uploadFile=function(){var e=document.getElementById("avatar-hidden-upload").files[0];if(e){var t=a.validateFile(e);if(t)return void v["default"].error(t);a.setState({image:e,preview:URL.createObjectURL(e),progress:0});var n=new FormData;n.append("avatar","upload"),n.append("image",e),h["default"].upload(a.props.user.api.avatar,n,function(e){a.setState({progress:e})}).then(function(e){a.setState({options:e,uploaded:e.detail}),v["default"].info(gettext("Your image has been uploaded and you may now crop it."))},function(e){400===e.status||413===e.status?(v["default"].error(e.detail),a.setState({isLoading:!1,image:null,progress:0})):a.props.showError(e)})}},a.state={image:null,preview:null,progress:0,uploaded:null,dataUrl:null},a}return l(t,e),s(t,[{key:"validateFile",value:function(e){if(e.size>this.props.options.upload.limit)return interpolate(gettext("Selected file is too big. (%(filesize)s)"),{filesize:(0,g["default"])(e.size)},!0);var t=gettext("Selected file type is not supported.");if(this.props.options.upload.allowed_mime_types.indexOf(e.type)===-1)return t;var a=!1,n=e.name.toLowerCase();return this.props.options.upload.allowed_extensions.map(function(e){n.substr(e.length*-1)===e&&(a=!0)}),!a&&t}},{key:"getUploadRequirements",value:function(e){var t=e.allowed_extensions.map(function(e){return e.substr(1)});return interpolate(gettext("%(files)s files smaller than %(limit)s"),{files:t.join(", "),limit:(0,g["default"])(e.limit)},!0)}},{key:"getUploadButton",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-upload"},u["default"].createElement(p["default"],{className:"btn-pick-file",onClick:this.pickFile},u["default"].createElement("div",{className:"material-icon"},"input"),gettext("Select file")),u["default"].createElement("p",{className:"text-muted"},this.getUploadRequirements(this.props.options.upload)))}},{key:"getUploadProgressLabel",value:function(){return interpolate(gettext("%(progress)s % complete"),{progress:this.state.progress},!0)}},{key:"getUploadProgress",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-upload"},u["default"].createElement("div",{className:"upload-progress"},u["default"].createElement("img",{src:this.state.preview}),u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar",role:"progressbar","aria-valuenow":"{this.state.progress}","aria-valuemin":"0","aria-valuemax":"100",style:{width:this.state.progress+"%"}},u["default"].createElement("span",{className:"sr-only"},this.getUploadProgressLabel())))))}},{key:"renderUpload",value:function(){return u["default"].createElement("div",null,u["default"].createElement("input",{type:"file",id:"avatar-hidden-upload",className:"hidden-file-upload",onChange:this.uploadFile}),this.state.image?this.getUploadProgress():this.getUploadButton(),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},u["default"].createElement(p["default"],{onClick:this.props.showIndex,disabled:!!this.state.image,className:"btn-default btn-block"},gettext("Cancel")))))}},{key:"renderCrop",value:function(){return u["default"].createElement(d["default"],{options:this.state.options,user:this.props.user,upload:this.state.uploaded,dataUrl:this.state.preview,onComplete:this.props.onComplete,showError:this.props.showError,showIndex:this.props.showIndex})}},{key:"render",value:function(){return this.state.uploaded?this.renderCrop():this.renderUpload()}}]),t}(u["default"].Component);a["default"]=y},{"../../services/ajax":361,"../../services/snackbar":372,"../../utils/file-size":380,"../button":7,"./crop":21,react:"react"}],26:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.dropdown?"btn btn-default btn-aligned btn-icon btn-dropdown-toggle open hidden-md hidden-lg":"btn btn-default btn-aligned btn-icon btn-dropdown-toggle hidden-md hidden-lg"}},{key:"render",value:function(){return u["default"].createElement("button",{className:this.getClassName(),type:"button",onClick:this.props.toggleNav,"aria-haspopup":"true","aria-expanded":this.props.dropdown?"true":"false"},u["default"].createElement("i",{className:"material-icon"},"menu"))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],27:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display;return t?o["default"].createElement(s["default"],{helpText:gettext("No profile details are editable at this time."),message:gettext("This option is currently unavailable.")}):null};var r=e("react"),o=n(r),l=e("../panel-message"),s=n(l)},{"../panel-message":91,react:"react"}],28:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../select"),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onChange=function(e){var t=n.props,a=t.field,r=t.onChange;r(a.fieldname,e.target.value)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props,t=e.disabled,a=e.field,n=e.value,r=a.input;return"select"===r.type?u["default"].createElement(d["default"],{choices:r.choices,disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,value:n}):"textarea"===r.type?u["default"].createElement("textarea",{className:"form-control",disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,rows:"4",type:"text",value:n}):"text"===r.type?u["default"].createElement("input",{className:"form-control",disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,type:"text",value:n}):null}}]),t}(u["default"].Component);a["default"]=f},{"../select":207,react:"react"}],29:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.disabled,a=e.errors,n=e.fields,r=e.name,l=e.onChange,i=e.value;return o["default"].createElement("fieldset",null,o["default"].createElement("legend",null,r),n.map(function(e){return o["default"].createElement(u["default"],{"for":"id_"+e.fieldname,helpText:e.help_text,key:e.fieldname,label:e.label,validation:a[e.fieldname]},o["default"].createElement(s["default"],{disabled:t,field:e,onChange:l,value:i[e.fieldname]}))}))};var r=e("react"),o=n(r),l=e("./field-input"),s=n(l),i=e("../form-group"),u=n(i)},{"../form-group":53,"./field-input":28,react:"react"}],30:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){var t=e.onCancel,a=e.disabled;return t?d["default"].createElement("button",{className:"btn btn-default",disabled:a,onClick:t,type:"button"},gettext("Cancel")):null}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.CancelButton=i;var c=e("react"),d=n(c),f=e("./fieldset"),p=n(f),m=e("../button"),h=n(m),b=e("../form"),v=n(b),_=e("../../services/ajax"),g=n(_),y=e("../../services/snackbar"),E=n(y),w=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.onChange=function(e,t){a.setState(r({},e,t))},a.state={isLoading:!1,errors:{}};for(var n=e.groups.length,s=0;s<n;s++)for(var i=e.groups[s],u=i.fields.length,c=0;c<u;c++){var d=i.fields[c].fieldname,f=i.fields[c].initial;a.state[d]=f}return a}return s(t,e),u(t,[{key:"send",value:function(){var e=Object.assign({},this.state,{errors:null,isLoading:null});return g["default"].post(this.props.api,e)}},{key:"handleSuccess",value:function(e){this.props.onSuccess(e)}},{key:"handleError",value:function(e){400===e.status?(E["default"].error(gettext("Form contains errors.")),this.setState({errors:e})):E["default"].apiError(e)}},{key:"render",value:function(){var e=this;return d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"panel-body"},this.props.groups.map(function(t,a){return d["default"].createElement(p["default"],{disabled:e.state.isLoading,errors:e.state.errors,fields:t.fields,name:t.name,key:a,onChange:e.onChange,value:e.state})})),d["default"].createElement("div",{className:"panel-footer text-right"},d["default"].createElement(i,{disabled:this.state.isLoading,onCancel:this.props.onCancel})," ",d["default"].createElement(h["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes"))))}}]),t}(v["default"]);a["default"]=w},{"../../services/ajax":361,"../../services/snackbar":372,"../button":7,"../form":54,"./fieldset":29,react:"react"}],31:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.api,a=e.display,n=e.groups,r=e.onCancel,o=e.onSuccess;return a?c["default"].createElement(b["default"],{api:t,groups:n,onCancel:r,onSuccess:o}):null}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.FormDisplay=s;var u=e("react"),c=n(u),d=e("./blankslate"),f=n(d),p=e("./loader"),m=n(p),h=e("./form"),b=n(h),v=e("../../services/ajax"),_=n(v),g=e("../../services/snackbar"),y=n(g),E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={loading:!0,groups:null},a}return l(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;_["default"].get(this.props.api).then(function(t){e.setState({loading:!1,groups:t})},function(t){y["default"].apiError(t),e.props.cancel&&e.props.cancel()})}},{key:"render",value:function(){var e=this.state,t=e.groups,a=e.loading;return c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Edit details"))),c["default"].createElement(m["default"],{display:a}),c["default"].createElement(f["default"],{display:!a&&!t.length}),c["default"].createElement(s,{api:this.props.api,display:!a&&t.length,groups:t,onCancel:this.props.onCancel,onSuccess:this.props.onSuccess}))}}]),t}(c["default"].Component);a["default"]=E},{"../../services/ajax":361,"../../services/snackbar":372,"./blankslate":27,"./form":30,"./loader":32,react:"react"}],32:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display;return t?o["default"].createElement("div",{className:"panel-body"},o["default"].createElement(s["default"],null)):null};var r=e("react"),o=n(r),l=e("../loader"),s=n(l)},{"../loader":56,react:"react"}],33:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.replaceSelection(n.props.execAction)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("button",{className:"btn btn-icon "+this.props.className,disabled:this.props.disabled,onClick:this.onClick,title:this.props.title,type:"button"},this.props.children)}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],34:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a=$.trim(prompt(gettext("Enter name of syntax of your code (optional)")+":"));t("\n\n```"+a+"\n"+e+"\n```\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert code")},e),s["default"].createElement("span",{className:"material-icon"},"functions"))},a.insertCode=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url");n(c)},{"../../../utils/is-url":381,"./action":33,react:"react"}],35:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("*"+e+"*")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Emphase selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_italic"))},a.makeEmphasis=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":33,react:"react"}],36:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){t("\n\n- - - - -\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert horizontal ruler")},e),s["default"].createElement("span",{className:"material-icon"},"remove"))},a.insertHr=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":33,react:"react"}],37:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a="",n="";e.length&&((0,d["default"])(e)?a=e:n=e),a=$.trim(prompt(gettext("Enter link to image")+":",a)),n=$.trim(prompt(gettext("Enter image label (optional)")+":",n)),a.length&&t(n.length>0?"!["+n+"]("+a+")":"!("+a+")")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert image")},e),s["default"].createElement("span",{className:"material-icon"},"insert_photo"))},a.insertImage=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url"),d=n(c)},{"../../../utils/is-url":381,"./action":33,react:"react"}],38:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a="",n="";return e.length&&((0,d["default"])(e)?a=e:n=e),a=$.trim(prompt(gettext("Enter link address")+":",a)||""),0!==a.length&&(n=$.trim(prompt(gettext("Enter link label (optional)")+":",n)),void(a.length&&t(n.length>0?"["+n+"]("+a+")":a)))}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert link")},e),s["default"].createElement("span",{className:"material-icon"},"insert_link"))},a.insertLink=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url"),d=n(c)},{"../../../utils/is-url":381,"./action":33,react:"react"}],39:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a=$.trim(prompt(gettext("Enter quote autor, prefix usernames with @")+":",a));t(a?'\n\n[quote="'+a+'"]\n'+e+"\n[/quote]\n\n":"\n\n[quote]\n"+e+"\n[/quote]\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert quote")},e),s["default"].createElement("span",{className:"material-icon"},"format_quote"))},a.insertQuote=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url");
-n(c)},{"../../../utils/is-url":381,"./action":33,react:"react"}],40:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("~~"+e+"~~")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Striketrough selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_strikethrough"))},a.makeStriketrough=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":33,react:"react"}],41:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("**"+e+"**")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Bolder selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_bold"))},a.makeStrong=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":33,react:"react"}],42:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.item.is_image?g["default"].createElement(i,e):g["default"].createElement(u,e)}function i(e){var t=e.item.url.thumb||e.item.url.index;return g["default"].createElement("div",{className:"editor-attachment-image"},g["default"].createElement("a",{href:e.item.url.index+"?shva=1",style:{backgroundImage:"url('"+t+"?shva=1')"},target:"_blank"}))}function u(e){return g["default"].createElement("div",{className:"editor-attachment-icon"},g["default"].createElement("span",{className:"material-icon"},"insert_drive_file"))}function c(e){return g["default"].createElement("h4",null,g["default"].createElement("a",{className:"item-title",href:e.item.url.index+"?shva=1",target:"_blank"},e.item.filename))}function d(e){var t=null;t=e.item.url.uploader?interpolate(j,{url:(0,w["default"])(e.item.url.uploader),user:(0,w["default"])(e.item.uploader_name)},!0):interpolate(x,{user:(0,w["default"])(e.item.uploader_name)},!0);var a=interpolate(N,{absolute:(0,w["default"])(e.item.uploaded_on.format("LLL")),relative:(0,w["default"])(e.item.uploaded_on.fromNow())},!0),n=interpolate((0,w["default"])(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")),{filetype:e.item.filetype,size:(0,k["default"])(e.item.size),uploader:t,uploaded_on:a},!0);return g["default"].createElement("p",{dangerouslySetInnerHTML:{__html:n}})}function f(e){return g["default"].createElement("div",{className:"editor-attachment-actions"},g["default"].createElement("div",{className:"row"},g["default"].createElement(p,e),g["default"].createElement(m,e),g["default"].createElement(h,e)))}function p(e){return e.item.isRemoved?null:g["default"].createElement("div",{className:"col-xs-6"},g["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onInsert,type:"button"},gettext("Insert")))}function m(e){return e.item.isRemoved&&e.item.acl.can_delete?null:g["default"].createElement("div",{className:"col-xs-6"},g["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onRemove,type:"button"},gettext("Remove")))}function h(e){return e.item.isRemoved?g["default"].createElement("div",{className:"col-xs-12"},g["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onUndo,type:"button"},gettext("Undo removal"))):null}Object.defineProperty(a,"__esModule",{value:!0});var b=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},v=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Preview=s,a.Image=i,a.Icon=u,a.Filename=c,a.Details=d,a.Actions=f,a.Insert=p,a.Remove=m,a.Undo=h;var _=e("react"),g=n(_),y=e("../../../.."),E=(n(y),e("../../../../utils/escape-html")),w=n(E),O=e("../../../../utils/file-size"),k=n(O),N='<abbr title="%(absolute)s">%(relative)s</abbr>',x='<span class="item-title">%(user)s</span>',j='<a href="%(url)s" class="item-title">%(user)s</a>',P=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onInsert=function(){n.props.replaceSelection(n.insertAttachment)},n.insertAttachment=function(e,t){var a=n.props.item;t(a.is_image?a.url.thumb?"[!["+a.filename+"]("+a.url.thumb+")]("+a.url.index+")":"[!["+a.filename+"]("+a.url.index+")]("+a.url.index+")":"["+a.filename+"]("+a.url.index+")")},n.onRemove=function(){n.updateItem({isRemoved:!0})},n.onUndo=function(){n.updateItem({isRemoved:!1})},n.updateItem=function(e){var t=n.props.attachments.map(function(t){return t.id===n.props.item.id?Object.assign({},t,e):t});n.props.onAttachmentsChange(t)},l=a,o(n,l)}return l(t,e),v(t,[{key:"render",value:function(){return g["default"].createElement("li",{className:"editor-attachment-complete"},g["default"].createElement("div",{className:"row"},g["default"].createElement("div",{className:"col-xs-12 col-sm-8 col-md-9"},g["default"].createElement(s,this.props),g["default"].createElement("div",{className:"editor-attachment-details"},g["default"].createElement(c,this.props),g["default"].createElement(d,this.props))),g["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3 xs-margin-top-half"},g["default"].createElement(f,b({onInsert:this.onInsert,onRemove:this.onRemove,onUndo:this.onUndo},this.props)))))}}]),t}(g["default"].Component);a["default"]=P},{"../../../..":299,"../../../../utils/escape-html":379,"../../../../utils/file-size":380,react:"react"}],43:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../../../utils/escape-html"),d=n(c),f="<strong>%(name)s</strong>",p=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=n.props.attachments.filter(function(e){return e.key!==n.props.item.key});n.props.onAttachmentsChange(e)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=interpolate(f,{name:(0,d["default"])(this.props.item.filename)},!0),t=interpolate(gettext("Error uploading %(filename)s"),{filename:e,progress:this.props.item.progress+"%"},!0);return u["default"].createElement("li",{className:"editor-attachment-error"},u["default"].createElement("div",{className:"editor-attachment-error-icon"},u["default"].createElement("span",{className:"material-icon"},"warning")),u["default"].createElement("div",{className:"editor-attachment-error-message"},u["default"].createElement("h4",{dangerouslySetInnerHTML:{__html:t+":"}}),u["default"].createElement("p",null,this.props.item.error),u["default"].createElement("button",{className:"btn btn-default btn-sm",onClick:this.onClick,type:"button"},gettext("Dismiss"))))}}]),t}(u["default"].Component);a["default"]=p},{"../../../../utils/escape-html":379,react:"react"}],44:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.item.id?o["default"].createElement(s["default"],e):e.item.error?o["default"].createElement(u["default"],e):o["default"].createElement(d["default"],e)};var r=e("react"),o=n(r),l=e("./complete"),s=n(l),i=e("./error"),u=n(i),c=e("./upload"),d=n(c),f=e("../../../.."),p=(n(f),e("../../../../utils/escape-html"));n(p)},{"../../../..":299,"../../../../utils/escape-html":379,"./complete":42,"./error":43,"./upload":45,react:"react"}],45:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=interpolate(i,{name:(0,s["default"])(e.item.filename)},!0),a=interpolate(gettext("Uploading %(filename)s... %(progress)s"),{filename:t,progress:e.item.progress+"%"},!0);return o["default"].createElement("li",{className:"editor-attachment-upload"},o["default"].createElement("div",{className:"editor-attachment-progress-bar"},o["default"].createElement("div",{className:"editor-attachment-progress",style:{width:e.item.progress+"%"}})),o["default"].createElement("p",{className:"editor-attachment-upload-message",dangerouslySetInnerHTML:{__html:a}}))};var r=e("react"),o=n(r),l=e("../../../../utils/escape-html"),s=n(l),i="<strong>%(name)s</strong>"},{"../../../../utils/escape-html":379,react:"react"}],46:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return d["default"].get("user").acl.max_attachment_size?o["default"].createElement("div",{className:"editor-attachments"},o["default"].createElement(s["default"],e),o["default"].createElement(u["default"],e)):null};var r=e("react"),o=n(r),l=e("./list"),s=n(l),i=e("./uploader"),u=n(i),c=e("../../.."),d=n(c)},{"../../..":299,"./list":47,"./uploader":49,react:"react"}],47:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return l["default"].createElement("ul",{className:"list-unstyled editor-attachments-list"},e.attachments.map(function(t){return l["default"].createElement(i["default"],r({item:t,key:t.id||t.key},e))}))};var o=e("react"),l=n(o),s=e("./attachment"),i=n(s)},{"./attachment":44,react:"react"}],48:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../.."),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){document.getElementById("editor-upload-field").click()},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return d["default"].get("user").acl.max_attachment_size?u["default"].createElement("button",{className:"btn btn-icon "+this.props.className,disabled:this.props.disabled,onClick:this.onClick,title:gettext("Upload file"),type:"button"},u["default"].createElement("span",{className:"material-icon"},"file_upload")):null}}]),t}(u["default"].Component);a["default"]=f},{"../../..":299,react:"react"}],49:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(){return"upld-"+Math.round((new Date).getTime())}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getRandomKey=s;var u=e("react"),c=n(u),d=e("moment"),f=n(d),p=e("../../.."),m=n(p),h=e("../../../services/ajax"),b=n(h),v=e("../../../services/snackbar"),_=n(v),g=function(e){function t(){var e,a,n,l;r(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.onChange=function(e){var t=e.target.files[0];if(t){var a={id:null,key:s(),progress:0,error:null,filename:t.name};n.props.onAttachmentsChange([a].concat(n.props.attachments));var r=new FormData;r.append("upload",t),b["default"].upload(m["default"].get("ATTACHMENTS_API"),r,function(e){a.progress=e,n.props.onAttachmentsChange(n.props.attachments.concat())}).then(function(e){e.uploaded_on=(0,f["default"])(e.uploaded_on),Object.assign(a,e),n.props.onAttachmentsChange(n.props.attachments.concat())},function(e){400===e.status||413===e.status?(a.error=e.detail,n.props.onAttachmentsChange(n.props.attachments.concat())):_["default"].apiError(e)})}},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("input",{id:"editor-upload-field",onChange:this.onChange,type:"file"})}}]),t}(c["default"].Component);a["default"]=g},{"../../..":299,"../../../services/ajax":361,"../../../services/snackbar":372,moment:"moment",react:"react"}],50:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){if(!e.canProtect)return null;var t=e.protect?gettext("Protected"):gettext("Protect");return d["default"].createElement("button",{className:"btn btn-icon btn-default btn-protect btn-sm pull-right",disabled:e.disabled,onClick:e.protect?e.onUnprotect:e.onProtect,title:t,type:"button"},d["default"].createElement("span",{className:"material-icon"},e.protect?"lock":"lock_outline"),d["default"].createElement("span",{className:"btn-text hidden-md hidden-lg"},t))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Protect=i;var c=e("react"),d=r(c),f=e("./actions/code"),p=r(f),m=e("./actions/emphasis"),h=r(m),b=e("./actions/hr"),v=r(b),_=e("./actions/image"),g=r(_),y=e("./actions/link"),E=r(y),w=e("./actions/striketrough"),O=r(w),k=e("./actions/strong"),N=r(k),x=e("./actions/quote"),j=r(x),P=e("./attachments"),C=r(P),M=e("./attachments/upload-button"),S=r(M),T=e("./markup-preview"),L=r(T),A=e("./textutils"),R=n(A),I=e("../button"),D=r(I),U=e("../.."),B=r(U),H=e("../../services/ajax"),z=r(H),F=e("../../services/modal"),q=r(F),G=e("../../services/snackbar"),Y=r(G),V=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onPreviewClick=function(){a.state.isPreviewLoading||(a.setState({isPreviewLoading:!0}),z["default"].post(B["default"].get("PARSE_MARKUP_API"),{post:a.props.value}).then(function(e){q["default"].show(d["default"].createElement(L["default"],{markup:e.parsed})),a.setState({isPreviewLoading:!1})},function(e){400===e.status?Y["default"].error(e.detail):Y["default"].apiError(e),a.setState({isPreviewLoading:!1})}))},a.replaceSelection=function(e){e(R.getSelectionText(),a._replaceSelection)},a._replaceSelection=function(e){a.props.onChange({target:{value:R.replace(e)}})},a.state={isPreviewLoading:!1},a}return s(t,e),u(t,[{key:"componentDidMount",value:function(){var e=this;$("#editor-textarea").atwho({at:"@",displayTpl:'<li><img src="${avatar}" alt="">${username}</li>',insertTpl:"@${username}",searchKey:"username",callbacks:{remoteFilter:function(e,t){$.getJSON(B["default"].get("MENTION_API"),{q:e},t)}}}),$("#editor-textarea").on("inserted.atwho",function(t,a,n){e.props.onChange(t)})}},{key:"render",value:function(){return d["default"].createElement("div",{className:"editor-border"},d["default"].createElement("textarea",{className:"form-control",value:this.props.value,disabled:this.props.loading,id:"editor-textarea",onChange:this.props.onChange,rows:"9"}),d["default"].createElement("div",{className:"editor-footer"},d["default"].createElement("div",{className:"buttons-list pull-left"},d["default"].createElement(N["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(h["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(O["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(v["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(E["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(g["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(j["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(p["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(S["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading})),d["default"].createElement(D["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,onClick:this.onPreviewClick,type:"button"},gettext("Preview")),d["default"].createElement(D["default"],{className:"btn-primary btn-sm pull-right",loading:this.props.loading},this.props.submitLabel||gettext("Post")),d["default"].createElement("button",{className:"btn btn-default btn-sm pull-right",disabled:this.props.loading,onClick:this.props.onCancel,type:"button"},gettext("Cancel")),d["default"].createElement("div",{className:"clearfix visible-xs-block"}),d["default"].createElement(i,{canProtect:this.props.canProtect,disabled:this.props.loading,onProtect:this.props.onProtect,onUnprotect:this.props.onUnprotect,protect:this.props.protect})),d["default"].createElement(C["default"],{attachments:this.props.attachments,onAttachmentsChange:this.props.onAttachmentsChange,placeholder:this.props.placeholder,replaceSelection:this.replaceSelection}))}}]),t}(d["default"].Component);a["default"]=V},{"../..":299,"../../services/ajax":361,"../../services/modal":367,"../../services/snackbar":372,"../button":7,"./actions/code":34,"./actions/emphasis":35,"./actions/hr":36,"./actions/image":37,"./actions/link":38,"./actions/quote":39,"./actions/striketrough":40,"./actions/strong":41,"./attachments":46,"./attachments/upload-button":48,"./markup-preview":51,"./textutils":52,react:"react"}],51:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"modal-dialog",role:"document"},o["default"].createElement("div",{className:"modal-content"},o["default"].createElement("div",{className:"modal-header"},o["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},o["default"].createElement("span",{"aria-hidden":"true"},"×")),o["default"].createElement("h4",{className:"modal-title"},gettext("Preview message"))),o["default"].createElement("div",{className:"modal-body markup-preview"},o["default"].createElement(s["default"],{markup:e.markup}))))};var r=e("react"),o=n(r),l=e("../misago-markup"),s=n(l)},{"../misago-markup":58,react:"react"}],52:[function(e,t,a){"use strict";function n(){return document.getElementById(d)}function r(){return document.getElementById(d).value}function o(e,t){return{start:e,end:t}}function l(){var e=n();if(document.selection){e.focus();var t=document.selection.createRange(),a=t.text.length;return t.moveStart("character",-e.value.length),o(t.text.length-a,t.text.length)}if(e.selectionStart||"0"==e.selectionStart)return o(e.selectionStart,e.selectionEnd)}function s(){var e=l();return $.trim(r().substring(e.start,e.end))}function i(e){var t=n();if(t.setSelectionRange)t.focus(),t.setSelectionRange(e.start,e.end);else if(t.createTextRange){var a=t.createTextRange();a.collapse(!0),a.moveStart("character",e.start),a.moveEnd("character",e.end),a.select()}}function u(e,t){var a=n(),r=a.value,l=r.substring(0,e.start);return a.value=r.substring(0,e.start)+t+r.substring(e.end),i(o(l.length+t.length,l.length+t.length)),a.value}function c(e){return u(l(),e)}Object.defineProperty(a,"__esModule",{value:!0}),a.getTextarea=n,a.getValue=r,a.getSelectionRange=o,a.getSelection=l,a.getSelectionText=s,a.setSelection=i,a._replace=u,a.replace=c;var d=a.textareaId="editor-textarea"},{}],53:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"isValidated",value:function(){return"undefined"!=typeof this.props.validation}},{key:"getClassName",value:function(){var e="form-group";return this.isValidated()&&(e+=" has-feedback",e+=null===this.props.validation?" has-success":" has-error"),e}},{key:"getFeedback",value:function(){var e=this;return this.props.validation?u["default"].createElement("div",{className:"help-block errors"},this.props.validation.map(function(t,a){return u["default"].createElement("p",{key:e.props["for"]+"FeedbackItem"+a},t)})):null}},{key:"getFeedbackDescription",value:function(){return this.isValidated()?u["default"].createElement("span",{id:this.props["for"]+"_status",className:"sr-only"},this.props.validation?gettext("(error)"):gettext("(success)")):null}},{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("label",{className:"control-label "+(this.props.labelClass||""),htmlFor:this.props["for"]||""},this.props.label+":"),u["default"].createElement("div",{className:this.props.controlClass||""},this.props.children,this.getFeedbackDescription(),this.getFeedback(),this.getHelpText(),this.props.extra||null))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],54:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../utils/validators"),f=e("../services/snackbar"),p=n(f),m=(0,d.required)(),h=function(e){function t(){var e,a,n,s;o(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.bindInput=function(e){return function(t){n.changeValue(e,t.target.value)}},n.changeValue=function(e,t){var a=r({},e,t),o=n.state.errors||{};o[e]=n.validateField(e,a[e]),a.errors=o,n.setState(a)},n.handleSubmit=function(e){if(e&&e.preventDefault(),!n.state.isLoading&&n.clean()){n.setState({isLoading:!0});var t=n.send();t?t.then(function(e){n.setState({isLoading:!1}),n.handleSuccess(e)},function(e){n.setState({isLoading:!1}),n.handleError(e)}):n.setState({isLoading:!1})}},s=a,l(n,s)}return s(t,e),i(t,[{key:"validate",value:function(){var e={};if(!this.state.validators)return e;var t={required:this.state.validators.required||this.state.validators,optional:this.state.validators.optional||{}},a=[];for(var n in t.required)t.required.hasOwnProperty(n)&&t.required[n]&&a.push(n);for(var r in t.optional)t.optional.hasOwnProperty(r)&&t.optional[r]&&a.push(r);for(var o in a){var l=a[o],s=this.validateField(l,this.state[l]);null===s?e[l]=null:s&&(e[l]=s)}return e}},{key:"isValid",value:function(){var e=this.validate();for(var t in e)if(e.hasOwnProperty(t)&&null!==e[t])return!1;return!0}},{key:"validateField",value:function(e,t){var a=[];if(!this.state.validators)return a;var n={required:(this.state.validators.required||this.state.validators)[e],optional:(this.state.validators.optional||{})[e]},r=m(t)||!1;if(n.required){if(r)a=[r];else for(var o in n.required){var l=n.required[o](t);l&&a.push(l)}return a.length?a:null}if(r===!1&&n.optional){for(var s in n.optional){var i=n.optional[s](t);i&&a.push(i)}return a.length?a:null}return!1}},{key:"clean",value:function(){return!0}},{key:"send",value:function(){return null}},{key:"handleSuccess",value:function(e){}},{key:"handleError",value:function(e){p["default"].apiError(e)}}]),t}(c["default"].Component);a["default"]=h},{"../services/snackbar":372,"../utils/validators":389,react:"react"}],55:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"isActive",value:function(){return this.props.isControlled?this.props.isActive:!!this.props.path&&0===document.location.pathname.indexOf(this.props.path)}},{key:"getClassName",value:function(){return this.isActive()?(this.props.className||"")+" "+(this.props.activeClassName||"active"):this.props.className||""}},{key:"render",value:function(){return u["default"].createElement("li",{className:this.getClassName()},this.props.children)}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],56:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{
-value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:e.className||"loader"},o["default"].createElement("div",{className:"loader-spinning-wheel"}))};var r=e("react"),o=n(r)},{react:"react"}],57:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.choices,a=e.onChange,n=e.value;return t?d["default"].createElement(v["default"],{label:gettext("Best answer"),helpText:gettext("Please select the best answer for your newly merged thread. No posts will be deleted during the merge."),"for":"id_best_answer"},d["default"].createElement("select",{className:"form-control",id:"id_best_answer",onChange:a,value:n},t.map(function(e){return d["default"].createElement("option",{value:e[0],key:e[0]},e[1])}))):null}function i(e){var t=e.choices,a=e.onChange,n=e.value;return t?d["default"].createElement(v["default"],{label:gettext("Poll"),helpText:gettext("Please select the poll for your newly merged thread. Rejected polls will be permanently deleted and cannot be recovered."),"for":"id_poll"},d["default"].createElement("select",{className:"form-control",id:"id_poll",onChange:a,value:n},t.map(function(e){return d["default"].createElement("option",{value:e[0],key:e[0]},e[1])}))):null}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.BestAnswerSelect=s,a.PollSelect=i;var c=e("react"),d=n(c),f=e("./button"),p=n(f),m=e("./form"),h=n(m),b=e("./form-group"),v=n(b),_=e("../services/ajax"),g=n(_),y=e("../services/modal"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleSuccess=function(e){a.props.onSuccess(e),E["default"].hide()},a.handleError=function(e){a.props.onError(e)},a.onBestAnswerChange=function(e){a.changeValue("bestAnswer",e.target.value)},a.onPollChange=function(e){a.changeValue("poll",e.target.value)},a.state={isLoading:!1,bestAnswer:"0",poll:"0"},a}return l(t,e),u(t,[{key:"clean",value:function(){if(this.props.polls&&"0"===this.state.poll){var e=confirm(gettext("Are you sure you want to delete all polls?"));return e}return!0}},{key:"send",value:function(){var e=Object.assign({},this.props.data,{best_answer:this.state.bestAnswer,poll:this.state.poll});return g["default"].post(this.props.api,e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Merge threads"))),d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(s,{choices:this.props.bestAnswers,onChange:this.onBestAnswerChange,value:this.state.bestAnswer}),d["default"].createElement(i,{choices:this.props.polls,onChange:this.onPollChange,value:this.state.poll})),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement(p["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Merge threads"))))))}}]),t}(h["default"]);a["default"]=w},{"../services/ajax":361,"../services/modal":367,"./button":7,"./form":54,"./form-group":53,react:"react"}],58:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../services/one-box"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){d["default"].render(this.documentNode)}},{key:"componentDidUpdate",value:function(e,t){d["default"].render(this.documentNode)}},{key:"shouldComponentUpdate",value:function(e,t){return e.markup!==this.props.markup}},{key:"render",value:function(){var e=this;return u["default"].createElement("article",{className:"misago-markup",dangerouslySetInnerHTML:{__html:this.props.markup},ref:function(t){e.documentNode=t}})}}]),t}(u["default"].Component);a["default"]=f},{"../services/one-box":368,react:"react"}],59:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body modal-loader"},u["default"].createElement(d["default"],null))}}]),t}(u["default"].Component);a["default"]=f},{"./loader":56,react:"react"}],60:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./panel-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},this.props.icon||"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.props.message),this.getHelpText(),u["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}}]),t}(d["default"]);a["default"]=f},{"./panel-message":91,react:"react"}],61:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.filter(function(e){return e.results.count>0});return t.map(function(e){return Object.assign({},e,{count:e.results.count,results:e.results.results.slice(0,n)})})};var n=5},{}],62:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.HEADER="HEADER",a.RESULT="RESULT",a.FOOTER="FOOTER"},{}],63:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.children,a=e.onChange,n=e.query;return o["default"].createElement("ul",{className:"dropdown-menu dropdown-search-results",role:"menu"},o["default"].createElement("li",{className:"form-group"},o["default"].createElement(s["default"],{value:n,onChange:a})),t)};var r=e("react"),o=n(r),l=e("./input"),s=n(l)},{"./input":67,react:"react"}],64:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return o["default"].createElement("li",{className:"dropdown-search-message"},gettext("Search returned no results."))};var r=e("react"),o=n(r)},{react:"react"}],65:[function(e,t,a){"use strict";function n(e,t){for(var a=e.length,n=0;n<a;n++){var l=e[n];t.push({provider:l,type:o.HEADER}),r(l,t)}}function r(e,t){for(var a=e.results.length,n=0;n<a;n++){var r=e.results[n];t.push({provider:e,result:r,type:o.RESULT})}t.push({provider:e,type:o.FOOTER})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=[];return n(e,t),t};var o=e("./constants")},{"./constants":62}],66:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){var t=e.isLoading,a=e.onChange,n=e.results,o=e.query;if(!o.trim().length)return l["default"].createElement(u["default"],{onChange:a,query:o});if(n.length){var i=(0,v["default"])(n);return l["default"].createElement(u["default"],{onChange:a,query:o},i.map(function(e){var t=e.type,a=e.provider,n=e.result;return t===s.RESULT?l["default"].createElement(h["default"],r({key:[a.id,t,n.id].join("_")},e)):l["default"].createElement(h["default"],r({key:[a.id,t].join("_"),query:o},e))}))}return t?l["default"].createElement(u["default"],{onChange:a,query:o},l["default"].createElement(p["default"],null)):l["default"].createElement(u["default"],{onChange:a,query:o},l["default"].createElement(d["default"],null))};var o=e("react"),l=n(o),s=e("./constants"),i=e("./dropdown-menu"),u=n(i),c=e("./empty"),d=n(c),f=e("./loader"),p=n(f),m=e("./result"),h=n(m),b=e("./flatten-results"),v=n(b)},{"./constants":62,"./dropdown-menu":63,"./empty":64,"./flatten-results":65,"./loader":68,"./result":71,react:"react"}],67:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.value,a=e.onChange;return o["default"].createElement("input",{"aria-haspopup":"true","aria-expanded":"false",autoComplete:"off",className:"form-control",value:t,onChange:a,placeholder:gettext("Search"),role:"combobox",type:"text"})};var r=e("react"),o=n(r)},{react:"react"}],68:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){e.message;return o["default"].createElement("li",{className:"dropdown-search-loader"},o["default"].createElement(s["default"],null))};var r=e("react"),o=n(r),l=e("../../loader"),s=n(l)},{"../../loader":56,react:"react"}],69:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.query,n=t.url+"?q="+encodeURI(a),r=ngettext('See full "%(provider)s" results page with %(count)s result.','See full "%(provider)s" results page with %(count)s results.',t.count);return o["default"].createElement("li",{className:"dropdown-search-footer"},o["default"].createElement("a",{href:n},interpolate(r,{count:t.count,provider:t.name},!0)))};var r=e("react"),o=n(r)},{react:"react"}],70:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider;return o["default"].createElement("li",{className:"dropdown-search-header"},t.name)};var r=e("react"),o=n(r)},{react:"react"}],71:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.result,n=e.type,r=e.query;return n===l.HEADER?o["default"].createElement(c["default"],{provider:t}):n===l.FOOTER?o["default"].createElement(i["default"],{provider:t,query:r}):o["default"].createElement(f["default"],{provider:t,result:a})};var r=e("react"),o=n(r),l=e("../constants"),s=e("./footer"),i=n(s),u=e("./header"),c=n(u),d=e("./result"),f=n(d)},{"../constants":62,"./footer":69,"./header":70,"./result":72,react:"react"}],72:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.result;return"threads"===t.id?o["default"].createElement(s["default"],{result:a}):o["default"].createElement(u["default"],{result:a})};var r=e("react"),o=n(r),l=e("./thread"),s=n(l),i=e("./user"),u=n(i)},{"./thread":73,"./user":74,react:"react"}],73:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.result,a=(t.poster,t.thread),n=gettext("Posted by %(poster)s on %(posted_on)s in %(category)s.");return s["default"].createElement("li",null,s["default"].createElement("a",{href:t.url.index,className:"dropdown-search-thread"},s["default"].createElement("h5",null,a.title),s["default"].createElement("small",{className:"dropdown-search-post-content"},$(t.content).text()),s["default"].createElement("small",{className:"dropdown-search-post-footer"},interpolate(n,{category:t.category.name,posted_on:(0,o["default"])(t.posted_on).format("LL"),poster:t.poster_name},!0))))};var r=e("moment"),o=n(r),l=e("react"),s=n(l)},{moment:"moment",react:"react"}],74:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.result,a=t.rank,n=gettext("%(title)s, joined on %(joined_on)s"),r=t.title||a.title||a.name;return s["default"].createElement("li",null,s["default"].createElement("a",{href:t.url,className:"dropdown-search-user"},s["default"].createElement("div",{className:"media"},s["default"].createElement("div",{className:"media-left"},s["default"].createElement(u["default"],{size:38,user:t})),s["default"].createElement("div",{className:"media-body"},s["default"].createElement("h5",{className:"media-heading"},t.username),s["default"].createElement("small",null,interpolate(n,{title:r,joined_on:(0,o["default"])(t.joined_on).format("LL")},!0))))))};var r=e("moment"),o=n(r),l=e("react"),s=n(l),i=e("../../../avatar"),u=n(i)},{"../../../avatar":5,moment:"moment",react:"react"}],75:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../services/ajax"),d=n(c),f=e("../../services/snackbar"),p=n(f),m=e("../.."),h=n(m),b=e("./clean-results"),v=n(b),_=e("./dropdown"),g=n(_),y=function(e){function t(){r(this,t);var e=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));return e.onToggle=function(t){e.setState(function(t,a){return t.isOpen||window.setTimeout(function(){e.container.querySelector("input").focus()},100),{isOpen:!t.isOpen}})},e.onDocumentMouseDown=function(t){for(var a=!0,n=t.target;null!==n&&n!==document;){if(n===e.container)return void(a=!1);n=n.parentNode}a&&e.setState({isOpen:!1})},e.onEscape=function(t){"Escape"===t.key&&e.setState({isOpen:!1})},e.onChange=function(t){var a=t.target.value;e.setState({query:a}),e.loadResults(a.trim())},e.state={isLoading:!1,isOpen:!1,query:"",results:[]},e.intervalId=null,e}return l(t,e),s(t,[{key:"componentDidMount",value:function(){document.addEventListener("mousedown",this.onDocumentMouseDown),document.addEventListener("keydown",this.onEscape)}},{key:"componentWillUnmount",value:function(){document.removeEventListener("mousedown",this.onDocumentMouseDown),document.removeEventListener("keydown",this.onEscape)}},{key:"loadResults",value:function(e){var t=this;if(e.length){var a=300+300*Math.random();this.intervalId&&window.clearTimeout(this.intervalId),this.setState({isLoading:!0}),this.intervalId=window.setTimeout(function(){d["default"].get(h["default"].get("SEARCH_API"),{q:e}).then(function(e){t.setState({intervalId:null,isLoading:!1,results:(0,v["default"])(e)})},function(e){p["default"].apiError(e),t.setState({intervalId:null,isLoading:!1,results:[]})})},a)}}},{key:"render",value:function(){var e=this,t="navbar-search dropdown";return this.state.isOpen&&(t+=" open"),u["default"].createElement("div",{className:t,ref:function(t){return e.container=t}},u["default"].createElement("a",{"aria-haspopup":"true","aria-expanded":"false",className:"navbar-icon","data-toggle":"dropdown",href:h["default"].get("SEARCH_URL"),onClick:this.onToggle},u["default"].createElement("i",{className:"material-icon"},"search")),u["default"].createElement(g["default"],{isLoading:this.state.isLoading,onChange:this.onChange,results:this.state.results,query:this.state.query}))}}]),t}(u["default"].Component);a["default"]=y},{"../..":299,"../../services/ajax":361,"../../services/snackbar":372,"./clean-results":61,"./dropdown":66,react:"react"}],76:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),o["default"].createElement(s["default"],null))};var r=e("react"),o=n(r),l=e("../../panel-loader"),s=n(l)},{"../../panel-loader":90,react:"react"}],77:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../panel-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.options.next_on?interpolate(gettext("You will be able to change your username %(next_change)s."),{next_change:this.props.options.next_on.fromNow()},!0):gettext("You have used up available name changes.")}},{key:"render",value:function(){return u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),u["default"].createElement(d["default"],{helpText:this.getHelpText(),message:gettext("You can't change your username at the moment.")}))}}]),t}(u["default"].Component);a["default"]=f},{"../../panel-message":91,react:"react"}],78:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../../services/ajax"),_=r(v),g=e("../../../services/snackbar"),y=r(g),E=e("../../../utils/validators"),w=n(E),O=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={username:"",validators:{username:[w.usernameContent(),w.usernameMinLength(e.options.length_min),w.usernameMaxLength(e.options.length_max)]},isLoading:!1},a}return s(t,e),i(t,[{key:"getHelpText",value:function(){var e=[];if(this.props.options.changes_left>0){var t=ngettext("You can change your username %(changes_left)s more time.","You can change your username %(changes_left)s more times.",this.props.options.changes_left);e.push(interpolate(t,{changes_left:this.props.options.changes_left},!0))}if(this.props.user.acl.name_changes_expire>0){var a=ngettext("Used changes become available again after %(name_changes_expire)s day.","Used changes become available again after %(name_changes_expire)s days.",this.props.user.acl.name_changes_expire);e.push(interpolate(a,{name_changes_expire:this.props.user.acl.name_changes_expire},!0))}return e.length?e.join(" "):null}},{key:"clean",value:function(){var e=this.validate();return e.username?(y["default"].error(e.username[0]),!1):this.state.username.trim()!==this.props.user.username||(y["default"].info(gettext("Your new username is same as current one.")),!1)}},{key:"send",value:function(){return _["default"].post(this.props.user.api.username,{username:this.state.username})}},{key:"handleSuccess",value:function(e){this.setState({username:""}),this.props.complete(e.username,e.slug,e.options)}},{key:"handleError",value:function(e){y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),c["default"].createElement("div",{className:"panel-body"},c["default"].createElement(b["default"],{label:gettext("New username"),"for":"id_username",helpText:this.getHelpText()},c["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change username")))))}}]),t}(m["default"]);a["default"]=O},{"../../../services/ajax":361,"../../../services/snackbar":372,"../../../utils/validators":389,"../../button":7,"../../form":54,"../../form-group":53,react:"react"}],79:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=e("./form-loading"),p=n(f),m=e("./form-locked"),h=n(m),b=e("./form"),v=n(b),_=e("../../username-history/root"),g=n(_),y=e("../../../index"),E=n(y),w=e("../../../reducers/username-history"),O=e("../../../reducers/users"),k=e("../../../services/ajax"),N=n(k),x=e("../../../services/page-title"),j=n(x),P=e("../../../services/snackbar"),C=n(P),M=e("../../../services/store"),S=n(M),T=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onComplete=function(e,t,n){a.setState({options:n}),S["default"].dispatch((0,w.addNameChange)({username:e,slug:t},a.props.user,a.props.user)),S["default"].dispatch((0,O.updateUsername)(a.props.user,e,t)),C["default"].success(gettext("Your username has been changed successfully."))},a.state={isLoaded:!1,options:null},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;j["default"].set({title:gettext("Change username"),parent:gettext("Change your options")}),Promise.all([N["default"].get(this.props.user.api.username),N["default"].get(E["default"].get("USERNAME_CHANGES_API"),{user:this.props.user.id})]).then(function(t){S["default"].dispatch((0,w.hydrate)(t[1].results)),e.setState({isLoaded:!0,options:{changes_left:t[0].changes_left,length_min:t[0].length_min,length_max:t[0].length_max,next_on:t[0].next_on?(0,u["default"])(t[0].next_on):null}})})}},{key:"getChangeForm",value:function(){return this.state.isLoaded?0===this.state.options.changes_left?d["default"].createElement(h["default"],{options:this.state.options}):d["default"].createElement(v["default"],{complete:this.onComplete,options:this.state.options,user:this.props.user}):d["default"].createElement(p["default"],null)}},{key:"render",value:function(){return d["default"].createElement("div",null,this.getChangeForm(),d["default"].createElement(g["default"],{changes:this.props["username-history"],isLoaded:this.state.isLoaded}))}}]),t}(d["default"].Component);a["default"]=T},{"../../../index":299,"../../../reducers/username-history":359,"../../../reducers/users":360,"../../../services/ajax":361,"../../../services/page-title":369,"../../../services/snackbar":372,"../../../services/store":373,"../../username-history/root":277,"./form":78,"./form-loading":76,"./form-locked":77,moment:"moment",react:"react"}],80:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../../services/ajax"),p=n(f),m=e("../../services/page-title"),h=n(m),b=e("../../services/snackbar"),v=n(b),_=e("../../services/store"),g=(n(_),e("../..")),y=n(g),E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onPasswordChange=function(e){a.setState({password:e.target.value})},a.handleSubmit=function(e){e.preventDefault();var t=a.state,n=t.isLoading,r=t.password,o=a.props.user;return 0==r.length?(v["default"].error(gettext("Enter your password to confirm account deletion.")),!1):!n&&(a.setState({isLoading:!0}),void p["default"].post(o.api["delete"],{password:r}).then(function(e){window.location.href=y["default"].get("MISAGO_PATH")},function(e){a.setState({isLoading:!1}),e.password?v["default"].error(e.password[0]):v["default"].apiError(e)}))},a.state={isLoading:!1,password:""},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){h["default"].set({title:gettext("Delete account"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement("form",{
-onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"panel panel-danger panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Delete account"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement("p",{className:"lead"},gettext("You are going to delete your account. This action is nonreversible, and will result in following data being deleted:")),u["default"].createElement("p",null,"- ",gettext("Stored IP addresses associated with content that you have posted will be deleted.")),u["default"].createElement("p",null,"- ",gettext("Your username will become available for other user to rename to or for new user to register their account with.")),u["default"].createElement("p",null,"- ",gettext("Your e-mail will become available for use in new account registration.")),u["default"].createElement("hr",null),u["default"].createElement("p",null,gettext("All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts."))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement("div",{className:"input-group"},u["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,name:"password-confirmation",type:"password",placeholder:gettext("Enter your password to confirm account deletion."),value:this.state.password,onChange:this.onPasswordChange}),u["default"].createElement("span",{className:"input-group-btn"},u["default"].createElement(d["default"],{className:"btn-danger",loading:this.state.isLoading},gettext("Delete my account")))))))}}]),t}(u["default"].Component);a["default"]=E},{"../..":299,"../../services/ajax":361,"../../services/page-title":369,"../../services/snackbar":372,"../../services/store":373,"../button":7,react:"react"}],81:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../edit-details"),d=n(c),f=e("../../services/page-title"),p=n(f),m=e("../../services/snackbar"),h=n(m),b=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onSuccess=function(){h["default"].info(gettext("Your details have been updated."))},l=a,o(n,l)}return l(t,e),s(t,[{key:"componentDidMount",value:function(){p["default"].set({title:gettext("Edit details"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement(d["default"],{api:this.props.user.api.edit_details,onSuccess:this.onSuccess})}}]),t}(u["default"].Component);a["default"]=b},{"../../services/page-title":369,"../../services/snackbar":372,"../edit-details":31,react:"react"}],82:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../form"),p=n(f),m=e("../form-group"),h=n(m),b=e("../select"),v=n(b),_=e("../yes-no-switch"),g=n(_),y=e("../../reducers/auth"),E=e("../../services/ajax"),w=n(E),O=e("../../services/page-title"),k=n(O),N=e("../../services/snackbar"),x=n(N),j=e("../../services/store"),P=n(j),C=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,is_hiding_presence:e.user.is_hiding_presence,limits_private_thread_invites_to:e.user.limits_private_thread_invites_to,subscribe_to_started_threads:e.user.subscribe_to_started_threads,subscribe_to_replied_threads:e.user.subscribe_to_replied_threads,errors:{}},a.privateThreadInvitesChoices=[{value:0,icon:"help_outline",label:gettext("Everybody")},{value:1,icon:"done_all",label:gettext("Users I follow")},{value:2,icon:"highlight_off",label:gettext("Nobody")}],a.subscribeToChoices=[{value:0,icon:"star_border",label:gettext("No")},{value:1,icon:"star_half",label:gettext("Notify")},{value:2,icon:"star",label:gettext("Notify with e-mail")}],a}return l(t,e),s(t,[{key:"send",value:function(){return w["default"].post(this.props.user.api.options,{is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,subscribe_to_started_threads:this.state.subscribe_to_started_threads,subscribe_to_replied_threads:this.state.subscribe_to_replied_threads})}},{key:"handleSuccess",value:function(){P["default"].dispatch((0,y.patch)({is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,subscribe_to_started_threads:this.state.subscribe_to_started_threads,subscribe_to_replied_threads:this.state.subscribe_to_replied_threads})),x["default"].success(gettext("Your forum options have been changed."))}},{key:"handleError",value:function(e){400===e.status?x["default"].error(gettext("Please reload page and try again.")):x["default"].apiError(e)}},{key:"componentDidMount",value:function(){k["default"].set({title:gettext("Forum options"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change forum options"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement("fieldset",null,u["default"].createElement("legend",null,gettext("Privacy settings")),u["default"].createElement(h["default"],{label:gettext("Hide my presence"),helpText:gettext("If you hide your presence, only members with permission to see hidden users will see when you are online."),"for":"id_is_hiding_presence"},u["default"].createElement(g["default"],{id:"id_is_hiding_presence",disabled:this.state.isLoading,iconOn:"visibility_off",iconOff:"visibility",labelOn:gettext("Hide my presence from other users"),labelOff:gettext("Show my presence to other users"),onChange:this.bindInput("is_hiding_presence"),value:this.state.is_hiding_presence})),u["default"].createElement(h["default"],{label:gettext("Private thread invitations"),"for":"id_limits_private_thread_invites_to"},u["default"].createElement(v["default"],{id:"id_limits_private_thread_invites_to",disabled:this.state.isLoading,onChange:this.bindInput("limits_private_thread_invites_to"),value:this.state.limits_private_thread_invites_to,choices:this.privateThreadInvitesChoices}))),u["default"].createElement("fieldset",null,u["default"].createElement("legend",null,gettext("Automatic subscriptions")),u["default"].createElement(h["default"],{label:gettext("Threads I start"),"for":"id_subscribe_to_started_threads"},u["default"].createElement(v["default"],{id:"id_subscribe_to_started_threads",disabled:this.state.isLoading,onChange:this.bindInput("subscribe_to_started_threads"),value:this.state.subscribe_to_started_threads,choices:this.subscribeToChoices})),u["default"].createElement(h["default"],{label:gettext("Threads I reply to"),"for":"id_subscribe_to_replied_threads"},u["default"].createElement(v["default"],{id:"id_subscribe_to_replied_threads",disabled:this.state.isLoading,onChange:this.bindInput("subscribe_to_replied_threads"),value:this.state.subscribe_to_replied_threads,choices:this.subscribeToChoices})))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes")))))}}]),t}(p["default"]);a["default"]=C},{"../../reducers/auth":346,"../../services/ajax":361,"../../services/page-title":369,"../../services/snackbar":372,"../../services/store":373,"../button":7,"../form":54,"../form-group":53,"../select":207,"../yes-no-switch":297,react:"react"}],83:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("div",{className:"list-group nav-side"},e.options.map(function(t){return s["default"].createElement(i.Link,{to:e.baseUrl+t.component+"/",className:"list-group-item",activeClassName:"active",key:t.component},s["default"].createElement("span",{className:"material-icon"},t.icon),t.name)}))}function o(e){return s["default"].createElement("ul",{className:e.className||"dropdown-menu stick-to-bottom",role:"menu"},e.options.map(function(t){return s["default"].createElement(c["default"],{path:e.baseUrl+t.component+"/",key:t.component},s["default"].createElement(i.Link,{to:e.baseUrl+t.component+"/",onClick:e.hideNav},s["default"].createElement("span",{className:"material-icon hidden-sm"},t.icon),t.name))}))}Object.defineProperty(a,"__esModule",{value:!0}),a.SideNav=r,a.CompactNav=o;var l=e("react"),s=n(l),i=e("react-router"),u=e("../li"),c=n(u),d=e("../../index");n(d)},{"../../index":299,"../li":55,react:"react","react-router":"react-router"}],84:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick,user:e.auth.user,"username-history":e["username-history"]}}function i(){var e=[{path:P["default"].get("USERCP_URL")+"forum-options/",component:(0,f.connect)(s)(y["default"])},{path:P["default"].get("USERCP_URL")+"edit-details/",component:(0,f.connect)(s)(_["default"])},{path:P["default"].get("USERCP_URL")+"change-username/",component:(0,f.connect)(s)(w["default"])},{path:P["default"].get("USERCP_URL")+"sign-in-credentials/",component:(0,f.connect)(s)(k["default"])}];return P["default"].get("ENABLE_DELETE_OWN_ACCOUNT")&&e.push({path:P["default"].get("USERCP_URL")+"delete-account/",component:(0,f.connect)(s)(b["default"])}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("../dropdown-toggle"),m=(n(p),e("./navs")),h=e("./delete-account"),b=n(h),v=e("./edit-details"),_=n(v),g=e("./forum-options"),y=n(g),E=e("./change-username/root"),w=n(E),O=e("./sign-in-credentials/root"),k=n(O),N=e("../with-dropdown"),x=n(N),j=e("../../index"),P=n(j),C=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-options"},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:"page-header"},d["default"].createElement("div",{className:"container"},d["default"].createElement("h1",null,gettext("Change your options"))),d["default"].createElement("div",{className:"page-tabs visible-xs-block visible-sm-block"},d["default"].createElement("div",{className:"container"},d["default"].createElement(m.CompactNav,{className:"nav nav-pills",baseUrl:P["default"].get("USERCP_URL"),options:P["default"].get("USER_OPTIONS")}))))),d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm"},d["default"].createElement(m.SideNav,{baseUrl:P["default"].get("USERCP_URL"),options:P["default"].get("USER_OPTIONS")})),d["default"].createElement("div",{className:"col-md-9"},this.props.children))))}}]),t}(x["default"]);a["default"]=C},{"../../index":299,"../dropdown-toggle":26,"../with-dropdown":296,"./change-username/root":79,"./delete-account":80,"./edit-details":81,"./forum-options":82,"./navs":83,"./sign-in-credentials/root":88,react:"react","react-redux":"react-redux"}],85:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e("../../../index"),s=n(l),i=function(){return o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Change email or password"))),o["default"].createElement("div",{className:"panel-body panel-message-body"},o["default"].createElement("div",{className:"message-icon"},o["default"].createElement("span",{className:"material-icon"},"info_outline")),o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",{className:"lead"},gettext("You need to set a password for your account to be able to change your username or email.")),o["default"].createElement("p",{className:"help-block"},o["default"].createElement("a",{className:"btn btn-primary",href:s["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Set password"))))))};a["default"]=i},{"../../../index":299,react:"react"}],86:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../../services/ajax"),_=r(v),g=e("../../../services/snackbar"),y=r(g),E=e("../../../utils/validators"),w=n(E),O=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={new_email:"",password:"",validators:{new_email:[w.email()],password:[]},isLoading:!1},a}return s(t,e),i(t,[{key:"clean",value:function(){var e=this.validate(),t=[this.state.new_email.trim().length,this.state.password.trim().length];return t.indexOf(0)!==-1?(y["default"].error(gettext("Fill out all fields.")),!1):!e.new_email||(y["default"].error(e.new_email[0]),!1)}},{key:"send",value:function(){return _["default"].post(this.props.user.api.change_email,{new_email:this.state.new_email,password:this.state.password})}},{key:"handleSuccess",value:function(e){this.setState({new_email:"",password:""}),y["default"].success(e.detail)}},{key:"handleError",value:function(e){400===e.status?e.new_email?y["default"].error(e.new_email):y["default"].error(e.password):y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("input",{type:"type",style:{display:"none"}}),c["default"].createElement("input",{type:"password",style:{display:"none"}}),c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Change e-mail address"))),c["default"].createElement("div",{className:"panel-body"},c["default"].createElement(b["default"],{label:gettext("New e-mail"),"for":"id_new_email"},c["default"].createElement("input",{type:"text",id:"id_new_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_email"),value:this.state.new_email})),c["default"].createElement("hr",null),c["default"].createElement(b["default"],{label:gettext("Your current password"),"for":"id_confirm_email"},c["default"].createElement("input",{type:"password",id:"id_confirm_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change e-mail")))))}}]),t}(m["default"]);a["default"]=O},{"../../../services/ajax":361,"../../../services/snackbar":372,"../../../utils/validators":389,"../../button":7,"../../form":54,"../../form-group":53,react:"react"}],87:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../../services/ajax"),v=n(b),_=e("../../../services/snackbar"),g=n(_),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={new_password:"",repeat_password:"",password:"",validators:{new_password:[],repeat_password:[],password:[]},isLoading:!1},a}return l(t,e),s(t,[{key:"clean",value:function(){var e=this.validate(),t=[this.state.new_password.trim().length,this.state.repeat_password.trim().length,this.state.password.trim().length];return t.indexOf(0)!==-1?(g["default"].error(gettext("Fill out all fields.")),!1):e.new_password?(g["default"].error(e.new_password[0]),!1):this.state.new_password===this.state.repeat_password||(g["default"].error(gettext("New passwords are different.")),!1)}},{key:"send",value:function(){return v["default"].post(this.props.user.api.change_password,{new_password:this.state.new_password,password:this.state.password})}},{key:"handleSuccess",value:function(e){this.setState({new_password:"",repeat_password:"",password:""}),g["default"].success(e.detail)}},{key:"handleError",value:function(e){400===e.status?e.new_password?g["default"].error(e.new_password):g["default"].error(e.password):g["default"].apiError(e)}},{key:"render",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("input",{type:"type",style:{display:"none"}}),u["default"].createElement("input",{type:"password",style:{display:"none"}}),u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change password"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement(h["default"],{label:gettext("New password"),"for":"id_new_password"},u["default"].createElement("input",{type:"password",id:"id_new_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_password"),value:this.state.new_password})),u["default"].createElement(h["default"],{label:gettext("Repeat password"),"for":"id_repeat_password"},u["default"].createElement("input",{type:"password",id:"id_repeat_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("repeat_password"),value:this.state.repeat_password})),u["default"].createElement("hr",null),u["default"].createElement(h["default"],{label:gettext("Your current password"),"for":"id_confirm_password"},u["default"].createElement("input",{type:"password",id:"id_confirm_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change password")))))}}]),t}(p["default"]);a["default"]=y},{"../../../services/ajax":361,"../../../services/snackbar":372,"../../button":7,"../../form":54,"../../form-group":53,react:"react"}],88:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change-email"),d=n(c),f=e("./change-password"),p=n(f),m=e("../../../index"),h=n(m),b=e("../../../services/page-title"),v=n(b),_=e("./UnusablePasswordMessage"),g=n(_),y=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){v["default"].set({title:gettext("Change email or password"),parent:gettext("Change your options")})}},{key:"render",value:function(){return this.props.user.has_usable_password?u["default"].createElement("div",null,u["default"].createElement(d["default"],{user:this.props.user}),u["default"].createElement(p["default"],{user:this.props.user}),u["default"].createElement("p",{className:"message-line"},u["default"].createElement("span",{className:"material-icon"},"warning"),u["default"].createElement("a",{href:h["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Change forgotten password")))):u["default"].createElement(g["default"],null)}}]),t}(u["default"].Component);a["default"]=y},{"../../../index":299,"../../../services/page-title":369,"./UnusablePasswordMessage":85,"./change-email":86,"./change-password":87,react:"react"}],89:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../utils/string-count"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.copy&&this.props.copy.length&&1===(0,d["default"])(this.props.copy,"<p")&&this.props.copy.indexOf("<br")===-1?"page-lead lead":"page-lead"}},{key:"render",value:function(){return this.props.copy&&this.props.copy.length?u["default"].createElement("div",{className:this.getClassName(),dangerouslySetInnerHTML:{__html:this.props.copy}}):null}}]),t}(u["default"].Component);a["default"]=f},{"../utils/string-count":388,react:"react"}],90:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"panel-body panel-body-loading"},u["default"].createElement(d["default"],{className:"loader loader-spaced"}))}}]),t}(u["default"].Component);a["default"]=f},{"./loader":56,react:"react"}],91:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"panel-body panel-message-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},this.props.icon||"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.props.message),this.getHelpText()))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],92:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../add-participant"),d=n(c),f=e("../../services/modal"),p=n(f),m=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){p["default"].show(u["default"].createElement(d["default"],{thread:n.props.thread}))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.thread.acl.can_add_participants?u["default"].createElement("div",{className:"col-xs-12 col-sm-3"},u["default"].createElement("button",{className:"btn btn-default btn-block",onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},"person_add"),gettext("Add participant"))):null}}]),t}(u["default"].Component);a["default"]=m},{"../../services/modal":367,"../add-participant":3,react:"react"}],93:[function(e,t,a){"use strict";
-function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function o(e,t){m["default"].patch(e.api.index,[{op:"remove",path:"participants",value:t.id}]).then(function(){b["default"].success(gettext("You have left this thread.")),window.setTimeout(function(){window.location=f["default"].get("PRIVATE_THREADS_URL")},3e3)},function(e){b["default"].apiError(e)})}function l(e,t){m["default"].patch(e.api.index,[{op:"remove",path:"participants",value:t.id},{op:"add",path:"acl",value:1}]).then(function(e){_["default"].dispatch((0,c.updateAcl)(e)),_["default"].dispatch(u.replace(e.participants));var a=gettext("%(user)s has been removed from this thread.");b["default"].success(interpolate(a,{user:t.username},!0))},function(e){b["default"].apiError(e)})}function s(e,t){m["default"].patch(e.api.index,[{op:"replace",path:"owner",value:t.id},{op:"add",path:"acl",value:1}]).then(function(e){_["default"].dispatch((0,c.updateAcl)(e)),_["default"].dispatch(u.replace(e.participants));var a=gettext("%(user)s has been made new thread owner.");b["default"].success(interpolate(a,{user:t.username},!0))},function(e){b["default"].apiError(e)})}Object.defineProperty(a,"__esModule",{value:!0}),a.leave=o,a.remove=l,a.changeOwner=s;var i=e("../../../reducers/participants"),u=r(i),c=e("../../../reducers/thread"),d=e("../../.."),f=n(d),p=e("../../../services/ajax"),m=n(p),h=e("../../../services/snackbar"),b=n(h),v=e("../../../services/store"),_=n(v)},{"../../..":299,"../../../reducers/participants":347,"../../../reducers/thread":356,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373}],94:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.isOwner;return t?l["default"].createElement("li",{className:"dropdown-header dropdown-header-owner"},l["default"].createElement("span",{className:"material-icon"},"start"),l["default"].createElement("span",{className:"icon-text"},gettext("Thread owner"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.participant,a="btn btn-default";return t.is_owner&&(a="btn btn-primary"),a+=" btn-user btn-block",l["default"].createElement("div",{className:"col-xs-12 col-sm-3 col-md-2 participant-card"},l["default"].createElement("div",{className:"dropdown"},l["default"].createElement("button",{"aria-haspopup":"true","aria-expanded":"false",className:a,"data-toggle":"dropdown",type:"button"},l["default"].createElement(f["default"],{size:"34",user:t}),l["default"].createElement("span",{className:"btn-text"},t.username)),l["default"].createElement("ul",{className:"dropdown-menu stick-to-bottom"},l["default"].createElement(r,{isOwner:t.is_owner}),l["default"].createElement("li",{className:"dropdown-header"}),l["default"].createElement("li",null,l["default"].createElement("a",{href:t.url},gettext("See profile"))),l["default"].createElement("li",{role:"separator",className:"divider"}),l["default"].createElement(i["default"],e),l["default"].createElement(c["default"],e))))},a.UserStatus=r;var o=e("react"),l=n(o),s=e("./make-owner"),i=n(s),u=e("./remove"),c=n(u),d=e("../../avatar"),f=n(d)},{"../../avatar":5,"./make-owner":96,"./remove":97,react:"react"}],95:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.participants,a=e.thread,n=e.user,r=e.userIsOwner;return o["default"].createElement("div",{className:"participants-cards"},o["default"].createElement("div",{className:"row"},t.map(function(e){return o["default"].createElement(s["default"],{key:e.id,participant:e,thread:a,user:n,userIsOwner:r})})))};var r=e("react"),o=n(r),l=e("./card"),s=n(l)},{"./card":94,react:"react"}],96:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./actions"),d=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onClick=function(){var e=!1;if(a.isUser)e=confirm(gettext("Are you sure you want to take over this thread?"));else{var t=gettext("Are you sure you want to change thread owner to %(user)s?");e=confirm(interpolate(t,{user:a.props.participant.username},!0))}e&&(0,c.changeOwner)(a.props.thread,a.props.participant)},a.isUser=e.participant.id===e.user.id,a}return l(t,e),s(t,[{key:"render",value:function(){return this.props.participant.is_owner?null:this.props.thread.acl.can_change_owner?u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},gettext("Make owner"))):null}}]),t}(u["default"].Component);a["default"]=d},{"./actions":93,react:"react"}],97:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./actions"),d=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onClick=function(){var e=!1;if(a.isUser)e=confirm(gettext("Are you sure you want to leave this thread?"));else{var t=gettext("Are you sure you want to remove %(user)s from this thread?");e=confirm(interpolate(t,{user:a.props.participant.username},!0))}e&&(a.isUser?(0,c.leave)(a.props.thread,a.props.participant):(0,c.remove)(a.props.thread,a.props.participant))},a.isUser=e.participant.id===e.user.id,a}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.user.acl.can_moderate_private_threads;return this.props.userIsOwner||this.isUser||e?u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},this.isUser?gettext("Leave thread"):gettext("Remove"))):null}}]),t}(u["default"].Component);a["default"]=d},{"./actions":93,react:"react"}],98:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){return t[0].id===e.id}Object.defineProperty(a,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return e.participants.length?i["default"].createElement("div",{className:"panel panel-default panel-participants"},i["default"].createElement("div",{className:"panel-body"},i["default"].createElement(f["default"],l({userIsOwner:o(e.user,e.participants)},e)),i["default"].createElement("div",{className:"row"},i["default"].createElement(c["default"],{thread:e.thread}),i["default"].createElement("div",{className:"col-xs-12 col-sm-9"},i["default"].createElement("p",null,m.getParticipantsCopy(e.participants)))))):null},a.getUserIsOwner=o;var s=e("react"),i=r(s),u=e("./add-participant"),c=r(u),d=e("./cards-list"),f=r(d),p=e("./utils"),m=n(p)},{"./add-participant":92,"./cards-list":95,"./utils":99,react:"react"}],99:[function(e,t,a){"use strict";function n(e){var t=e.length,a=ngettext("This thread has %(users)s participant.","This thread has %(users)s participants.",t);return interpolate(a,{users:t},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a.getParticipantsCopy=n},{}],100:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LABELS=a.STYLES=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../services/zxcvbn"),d=n(c),f=a.STYLES=["progress-bar-danger","progress-bar-warning","progress-bar-warning","progress-bar-primary","progress-bar-success"],p=a.LABELS=[gettext("Entered password is very weak."),gettext("Entered password is weak."),gettext("Entered password is average."),gettext("Entered password is strong."),gettext("Entered password is very strong.")],m=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a._score=0,a._password=null,a._inputs=[],a.state={loaded:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;d["default"].load().then(function(){e.setState({loaded:!0})})}},{key:"getScore",value:function(e,t){var a=this,n=!1;return e!==this._password&&(n=!0),t.length!==this._inputs.length?n=!0:t.map(function(e,t){e.trim()!==a._inputs[t]&&(n=!0)}),n&&(this._score=d["default"].scorePassword(e,t),this._password=e,this._inputs=t.map(function(e){return e.trim()})),this._score}},{key:"render",value:function(){if(!this.state.loaded)return null;var e=this.getScore(this.props.password,this.props.inputs);return u["default"].createElement("div",{className:"help-block password-strength"},u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar "+f[e],style:{width:20+20*e+"%"},role:"progress-bar","aria-valuenow":e,"aria-valuemin":"0","aria-valuemax":"4"},u["default"].createElement("span",{className:"sr-only"},p[e]))),u["default"].createElement("p",{className:"text-small"},p[e]))}}]),t}(u["default"].Component);a["default"]=m},{"../services/zxcvbn":374,react:"react"}],101:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(){for(var e="";12!=e.length;)e=Math.random().toString(36).replace(/[^a-zA-Z0-9]+/g,"").substr(1,12);return e}Object.defineProperty(a,"__esModule",{value:!0}),a.PollChoice=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.generateRandomHash=s;var u=e("react"),c=n(u),d=function(e){function t(){var e,a,n,l;r(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.onAdd=function(){var e=n.props.choices.slice();e.push({hash:s(),label:""}),n.props.setChoices(e)},n.onChange=function(e,t){var a=n.props.choices.map(function(a){return a.hash===e&&(a.label=t),a});n.props.setChoices(a)},n.onDelete=function(e){var t=n.props.choices.filter(function(t){return t.hash!==e});n.props.setChoices(t)},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"poll-choices-control"},c["default"].createElement("ul",{className:"list-group"},this.props.choices.map(function(t){return c["default"].createElement(f,{canDelete:e.props.choices.length>2,choice:t,disabled:e.props.disabled,key:t.hash,onChange:e.onChange,onDelete:e.onDelete})})),c["default"].createElement("button",{className:"btn btn-default btn-sm",disabled:this.props.disabled,onClick:this.onAdd,type:"button"},gettext("Add choice")))}}]),t}(c["default"].Component);a["default"]=d;var f=a.PollChoice=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onChange=function(e){n.props.onChange(n.props.choice.hash,e.target.value)},n.onDelete=function(){var e=confirm(gettext("Are you sure you want to delete this choice?"));e&&n.props.onDelete(n.props.choice.hash)},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("li",{className:"list-group-item"},c["default"].createElement("button",{className:"btn",disabled:!this.props.canDelete||this.props.disabled,onClick:this.onDelete,title:gettext("Delete this choice"),type:"button"},c["default"].createElement("span",{className:"material-icon"},"close")),c["default"].createElement("input",{disabled:this.props.disabled,maxLength:"255",placeholder:gettext("choice label"),type:"text",onChange:this.onChange,value:this.props.choice.label}))}}]),t}(c["default"].Component)},{react:"react"}],102:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.isEdit?null:d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(g["default"],{label:gettext("Make voting public"),helpText:gettext("Making voting public will allow everyone to access detailed list of votes, showing which users voted for which choices and at which times. This option can't be changed after poll's creation. Moderators may see voting details for all polls."),"for":"id_is_public"},d["default"].createElement(E["default"],{id:"id_is_public",disabled:e.disabled,iconOn:"visibility",iconOff:"visibility_off",labelOn:gettext("Votes are public"),labelOff:gettext("Votes are hidden"),onChange:e.bindInput("is_public"),value:e.value})))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.PollPublicSwitch=i;var c=e("react"),d=r(c),f=e("./choices-control"),p=r(f),m=e("../../button"),h=r(m),b=e("../../form"),v=r(b),_=e("../../form-group"),g=r(_),y=e("../../yes-no-switch"),E=r(y),w=e("../../../reducers/poll"),O=n(w),k=e("../../../services/ajax"),N=r(k),x=e("../../../services/posting"),j=r(x),P=e("../../../services/snackbar"),C=r(P),M=e("../../../services/store"),S=r(M),T=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.setChoices=function(e){var t=Object.assign({},t,{choices:null});a.setState({choices:e,errors:t})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard poll?"));e&&j["default"].close()};var n=e.poll||{question:"",choices:[{hash:"choice-10000",label:""},{hash:"choice-20000",label:""}],length:0,allowed_choices:1,allow_revotes:0,is_public:0};return a.state={isLoading:!1,isEdit:!!n.question,question:n.question,choices:n.choices,length:n.length,allowed_choices:n.allowed_choices,allow_revotes:n.allow_revotes,is_public:n.is_public,validators:{question:[],choices:[],length:[],allowed_choices:[]},errors:{}},a}return s(t,e),u(t,[{key:"send",value:function(){var e={question:this.state.question,choices:this.state.choices,length:this.state.length,allowed_choices:this.state.allowed_choices,allow_revotes:this.state.allow_revotes,is_public:this.state.is_public};return this.state.isEdit?N["default"].put(this.props.poll.api.index,e):N["default"].post(this.props.thread.api.poll,e)}},{key:"handleSuccess",value:function(e){S["default"].dispatch(O.replace(e)),this.state.isEdit?C["default"].success(gettext("Poll has been edited.")):C["default"].success(gettext("Poll has been posted.")),j["default"].close()}},{key:"handleError",value:function(e){400===e.status?(e.non_field_errors&&(e.allowed_choices=e.non_field_errors),this.setState({errors:Object.assign({},e)}),C["default"].error(gettext("Form contains errors."))):C["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"poll-form"},d["default"].createElement("div",{className:"container"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"panel panel-default panel-form"},d["default"].createElement("div",{className:"panel-body"},d["default"].createElement("fieldset",null,d["default"].createElement("legend",null,gettext("Question and choices")),d["default"].createElement(g["default"],{label:gettext("Poll question"),"for":"id_questions",validation:this.state.errors.question},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_questions",onChange:this.bindInput("question"),type:"text",maxLength:"255",value:this.state.question})),d["default"].createElement(g["default"],{label:gettext("Available choices"),validation:this.state.errors.choices},d["default"].createElement(p["default"],{choices:this.state.choices,disabled:this.state.isLoading,setChoices:this.setChoices}))),d["default"].createElement("fieldset",null,d["default"].createElement("legend",null,gettext("Voting")),d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(g["default"],{label:gettext("Poll length"),helpText:gettext("Enter number of days for which voting in this poll should be possible or zero to run this poll indefinitely."),"for":"id_length",validation:this.state.errors.length},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_length",onChange:this.bindInput("length"),type:"text",value:this.state.length}))),d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(g["default"],{label:gettext("Allowed choices"),"for":"id_allowed_choices",validation:this.state.errors.allowed_choices},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_allowed_choices",onChange:this.bindInput("allowed_choices"),type:"text",maxLength:"255",value:this.state.allowed_choices})))),d["default"].createElement("div",{className:"row"},d["default"].createElement(i,{bindInput:this.bindInput,disabled:this.state.isLoading,isEdit:this.state.isEdit,value:this.state.is_public}),d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(g["default"],{label:gettext("Allow vote changes"),"for":"id_allow_revotes"},d["default"].createElement(E["default"],{id:"id_allow_revotes",disabled:this.state.isLoading,iconOn:"check",iconOff:"close",labelOn:gettext("Allow participants to change their vote"),labelOff:gettext("Don't allow participants to change their vote"),onChange:this.bindInput("allow_revotes"),value:this.state.allow_revotes})))))),d["default"].createElement("div",{className:"panel-footer text-right"},d["default"].createElement("button",{className:"btn btn-default",disabled:this.state.isLoading,onClick:this.onCancel,type:"button"},gettext("Cancel"))," ",d["default"].createElement(h["default"],{className:"btn-primary",loading:this.state.isLoading},this.state.isEdit?gettext("Save changes"):gettext("Post poll")))))))}}]),t}(v["default"]);a["default"]=T},{"../../../reducers/poll":348,"../../../services/ajax":361,"../../../services/posting":371,"../../../services/snackbar":372,"../../../services/store":373,"../../button":7,"../../form":54,"../../form-group":53,"../../yes-no-switch":297,"./choices-control":101,react:"react"}],103:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PollForm=a.Poll=void 0;var r=e("./poll"),o=n(r),l=e("./form"),s=n(l);a.Poll=o["default"],a.PollForm=s["default"]},{"./form":102,"./poll":105}],104:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=interpolate((0,m["default"])(gettext("Posted by %(poster)s %(posted_on)s.")),{poster:o(e.poll),posted_on:l(e.poll)},!0);return f["default"].createElement("li",{className:"poll-info-creation",dangerouslySetInnerHTML:{__html:t}})}function o(e){return e.url.poster?interpolate(v,{url:(0,m["default"])(e.url.poster),user:(0,m["default"])(e.poster_name)},!0):interpolate(b,{user:(0,m["default"])(e.poster_name)},!0)}function l(e){return interpolate(h,{absolute:(0,m["default"])(e.posted_on.format("LLL")),relative:(0,m["default"])(e.posted_on.fromNow())},!0)}function s(e){if(!e.poll.length)return null;var t=interpolate((0,m["default"])(gettext("Voting ends %(ends_on)s.")),{ends_on:i(e.poll)},!0);return f["default"].createElement("li",{className:"poll-info-ends-on",dangerouslySetInnerHTML:{__html:t}})}function i(e){return interpolate(h,{absolute:(0,m["default"])(e.endsOn.format("LLL")),relative:(0,m["default"])(e.endsOn.fromNow())},!0)}function u(e){var t=ngettext("%(votes)s vote.","%(votes)s votes.",e.votes),a=interpolate(t,{votes:e.votes},!0);return f["default"].createElement("li",{className:"poll-info-votes"},a)}function c(e){return e.poll.is_public?f["default"].createElement("li",{className:"poll-info-public"},gettext("Votes are public.")):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return f["default"].createElement("ul",{className:"list-unstyled list-inline poll-details"},f["default"].createElement(u,{votes:e.poll.votes}),f["default"].createElement(s,{poll:e.poll}),f["default"].createElement(c,{poll:e.poll}),f["default"].createElement(r,{poll:e.poll}))},a.PollCreation=r,a.getPoster=o,a.getPostedOn=l,a.PollLength=s,a.getEndsOn=i,a.PollVotes=u,a.PollIsPublic=c;var d=e("react"),f=n(d),p=e("../../utils/escape-html"),m=n(p),h='<abbr title="%(absolute)s">%(relative)s</abbr>',b='<span class="item-title">%(user)s</span>',v='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../utils/escape-html":379,react:"react"}],105:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return!!e.length&&(0,p["default"])().isAfter(e.endsOn)}Object.defineProperty(a,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getIsPollOver=s;var c=e("react"),d=n(c),f=e("moment"),p=n(f),m=e("./results"),h=n(m),b=e("./voting"),v=n(b),_=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.showResults=function(){a.setState({showResults:!0})},a.showVoting=function(){a.setState({showResults:!1})};var n=!0;return e.user.id&&!e.poll.hasSelectedChoices&&(n=!1),a.state={showResults:n},a}return l(t,e),u(t,[{key:"render",value:function(){if(!this.props.thread.poll)return null;var e=s(this.props.poll);return e||!this.props.poll.acl.can_vote||this.state.showResults?d["default"].createElement(h["default"],i({isPollOver:e,showVoting:this.showVoting},this.props)):d["default"].createElement(v["default"],i({showResults:this.showResults},this.props))}}]),t}(d["default"].Component);a["default"]=_},{"./results":107,"./voting":111,moment:"moment",react:"react"}],106:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=0;return e.choice.votes&&e.poll.votes&&(t=Math.ceil(100*e.choice.votes/e.poll.votes)),u["default"].createElement("dl",{className:"dl-horizontal"},u["default"].createElement("dt",null,e.choice.label),u["default"].createElement("dd",null,u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar",role:"progressbar","aria-valuenow":t,"aria-valuemin":"0","aria-valuemax":"100",style:{width:t+"%"}},u["default"].createElement("span",{className:"sr-only"},l(e.votes,e.proc)))),u["default"].createElement("ul",{className:"list-unstyled list-inline poll-chart"},u["default"].createElement(o,{proc:t,votes:e.choice.votes}),u["default"].createElement(s,{selected:e.choice.selected}))))}function o(e){return u["default"].createElement("li",{className:"poll-chart-votes"},l(e.votes,e.proc))}function l(e,t){var a=ngettext("%(votes)s vote, %(proc)s% of total.","%(votes)s votes, %(proc)s% of total.",e);return interpolate(a,{votes:e,proc:t},!0)}function s(e){return e.selected?u["default"].createElement("li",{className:"poll-chart-selected"},u["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Your choice.")):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return u["default"].createElement("div",{className:"poll-choices-bars"},e.poll.choices.map(function(t){return u["default"].createElement(r,{choice:t,key:t.hash,poll:e.poll})}))},a.PollChoice=r,a.ChoiceVotes=o,a.getVotesLabel=l,a.UserChoice=s;var i=e("react"),u=n(i)},{react:"react"}],107:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"panel panel-default panel-poll"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("h2",null,e.poll.question),o["default"].createElement(d["default"],{poll:e.poll}),o["default"].createElement(s["default"],{poll:e.poll}),o["default"].createElement(u["default"],{isPollOver:e.isPollOver,poll:e.poll,showVoting:e.showVoting,thread:e.thread})))};var r=e("react"),o=n(r),l=e("./chart"),s=n(l),i=e("./options"),u=n(i),c=e("../info"),d=n(c)},{"../info":104,"./chart":106,"./options":109,react:"react"}],108:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.isLoading?v["default"].createElement(O["default"],null):e.error?v["default"].createElement(E["default"],{icon:"error_outline",message:e.error}):v["default"].createElement(i,{data:e.data})}function i(e){return v["default"].createElement("div",{className:"modal-body modal-poll-votes"},v["default"].createElement("ul",{className:"list-unstyled votes-details"},e.data.map(function(e){return v["default"].createElement(u,m({key:e.hash},e))})))}function u(e){return v["default"].createElement("li",null,v["default"].createElement("h4",null,e.label),v["default"].createElement(c,{votes:e.votes}),v["default"].createElement(d,{voters:e.voters}),v["default"].createElement("hr",null))}function c(e){var t=ngettext("%(votes)s user has voted for this choice.","%(votes)s users have voted for this choice.",e.votes),a=interpolate(t,{votes:e.votes},!0);return v["default"].createElement("p",null,a)}function d(e){return e.voters.length?v["default"].createElement("ul",{className:"list-unstyled"},e.voters.map(function(e){return v["default"].createElement(f,m({key:e.username},e))})):null}function f(e){return e.url?v["default"].createElement("li",null,v["default"].createElement("a",{className:"item-title",href:e.url},e.username)," ",v["default"].createElement(p,{voted_on:e.voted_on})):v["default"].createElement("li",null,v["default"].createElement("strong",null,e.username)," ",v["default"].createElement(p,{voted_on:e.voted_on}))}function p(e){return v["default"].createElement("abbr",{className:"text-muted",title:e.voted_on.format("LLL")},e.voted_on.fromNow())}Object.defineProperty(a,"__esModule",{value:!0});var m=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},h=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),
-n&&e(t,n),t}}();a.ModalBody=s,a.ChoicesList=i,a.ChoiceDetails=u,a.VotesCount=c,a.VotesList=d,a.Voter=f,a.VoteDate=p;var b=e("react"),v=n(b),_=e("moment"),g=n(_),y=e("../../modal-message"),E=n(y),w=e("../../modal-loader"),O=n(w),k=e("../../../services/ajax"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!0,error:null,data:[]},a}return l(t,e),h(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.poll.api.votes).then(function(t){var a=t.map(function(e){return Object.assign({},e,{voters:e.voters.map(function(e){return Object.assign({},e,{voted_on:(0,g["default"])(e.voted_on)})})})});e.setState({isLoading:!1,data:a})},function(t){e.setState({isLoading:!1,error:t.detail})})}},{key:"render",value:function(){return v["default"].createElement("div",{className:"modal-dialog"+(this.state.error?" modal-message":" modal-sm"),role:"document"},v["default"].createElement("div",{className:"modal-content"},v["default"].createElement("div",{className:"modal-header"},v["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},v["default"].createElement("span",{"aria-hidden":"true"},"×")),v["default"].createElement("h4",{className:"modal-title"},gettext("Poll votes"))),v["default"].createElement(s,{data:this.state.data,error:this.state.error,isLoading:this.state.isLoading})))}}]),t}(v["default"].Component);a["default"]=x},{"../../../services/ajax":361,"../../modal-loader":59,"../../modal-message":60,moment:"moment",react:"react"}],109:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e,t,a){return a.is_public||t.can_delete||t.can_edit||t.can_see_votes||t.can_vote&&!e&&(!a.hasSelectedChoices||a.allow_revotes)}function u(e,t){var a="col-xs-6";return 1===e.length&&(a="col-xs-12"),3===e.length&&e[0]===t&&(a="col-xs-12"),a+" col-sm-3 col-md-2"}function c(e){var t=e.poll.acl.can_vote,a=!e.poll.hasSelectedChoices||e.poll.allow_revotes;return t&&a?p["default"].createElement("div",{className:u(e.controls,0)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:e.poll.isBusy,onClick:e.showVoting,type:"button"},gettext("Vote"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Edit=a.SeeVotes=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){var t=e.isPollOver,a=e.poll,n=e.showVoting,r=e.thread;if(!i(t,a.acl,a))return null;var o=[],l=a.acl.can_vote,s=!a.hasSelectedChoices||a.allow_revotes;return l&&s&&o.push(0),(a.is_public||a.acl.can_see_votes)&&o.push(1),a.acl.can_edit&&o.push(2),a.acl.can_delete&&o.push(3),p["default"].createElement("div",{className:"row poll-options"},p["default"].createElement(c,{controls:o,isPollOver:t,poll:a,showVoting:n}),p["default"].createElement(M,{controls:o,poll:a}),p["default"].createElement(S,{controls:o,poll:a,thread:r}),p["default"].createElement(T,{controls:o,poll:a}))},a.isVisible=i,a.getClassName=u,a.ChangeVote=c;var f=e("react"),p=r(f),m=e("./modal"),h=r(m),b=e("../../../reducers/poll"),v=n(b),_=e("../../../reducers/thread"),g=n(_),y=e("../../../services/ajax"),E=r(y),w=e("../../../services/modal"),O=r(w),k=e("../../../services/posting"),N=r(k),x=e("../../../services/snackbar"),j=r(x),P=e("../../../services/store"),C=r(P),M=a.SeeVotes=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){O["default"].show(p["default"].createElement(h["default"],{poll:n.props.poll}))},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){var e=this.props.poll.is_public||this.props.poll.acl.can_see_votes;return e?p["default"].createElement("div",{className:u(this.props.controls,1)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("See votes"))):null}}]),t}(p["default"].Component),S=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){N["default"].open({submit:n.props.poll.api.index,thread:n.props.thread,poll:n.props.poll,mode:"POLL"})},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){return this.props.poll.acl.can_edit?p["default"].createElement("div",{className:u(this.props.controls,2)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("Edit"))):null}}]),t}(p["default"].Component),T=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=confirm(gettext("Are you sure you want to delete this poll? This action is not reversible."));return!!e&&(C["default"].dispatch(v.busy()),void E["default"]["delete"](n.props.poll.api.index).then(n.handleSuccess,n.handleError))},n.handleSuccess=function(e){j["default"].success("Poll has been deleted"),C["default"].dispatch(v.remove()),C["default"].dispatch(g.updateAcl(e))},n.handleError=function(e){j["default"].apiError(e),C["default"].dispatch(v.release())},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){return this.props.poll.acl.can_delete?p["default"].createElement("div",{className:u(this.props.controls,3)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("Delete"))):null}}]),t}(p["default"].Component)},{"../../../reducers/poll":348,"../../../reducers/thread":356,"../../../services/ajax":361,"../../../services/modal":367,"../../../services/posting":371,"../../../services/snackbar":372,"../../../services/store":373,"./modal":108,react:"react"}],110:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.choicesLeft;if(0===t)return s["default"].createElement("li",{className:"poll-help-choices-left"},gettext("You can't select any more choices."));var a=ngettext("You can select %(choices)s more choice.","You can select %(choices)s more choices.",t),n=interpolate(a,{choices:t},!0);return s["default"].createElement("li",{className:"poll-help-choices-left"},n)}function o(e){return e.poll.allow_revotes?s["default"].createElement("li",{className:"poll-help-allow-revotes"},gettext("You can change your vote later.")):s["default"].createElement("li",{className:"poll-help-no-revotes"},gettext("Votes are final."))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return s["default"].createElement("ul",{className:"list-unstyled list-inline poll-help"},s["default"].createElement(r,{choicesLeft:e.choicesLeft}),s["default"].createElement(o,{poll:e.poll}))},a.PollChoicesLeft=r,a.PollAllowRevote=o;var l=e("react"),s=n(l),i=e("../../../utils/escape-html");n(i)},{"../../../utils/escape-html":379,react:"react"}],111:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./help"),f=r(d),p=e("./select"),m=r(p),h=e("./utils"),b=e("../info"),v=r(b),_=e("../results/options"),g=e("../../button"),y=r(g),E=e("../../form"),w=r(E),O=e("../../../reducers/poll"),k=n(O),N=e("../../../services/ajax"),x=r(N),j=e("../../../services/snackbar"),P=r(j),C=e("../../../services/store"),M=r(C),S=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.toggleChoice=function(e){var t=(0,h.getChoiceFromHash)(a.state.choices,e),n=null;n=t.selected?a.deselectChoice(t,e):a.selectChoice(t,e),a.setState({choices:n,choicesLeft:(0,h.getChoicesLeft)(a.props.poll,n)})},a.selectChoice=function(e,t){var n=(0,h.getChoicesLeft)(a.props.poll,a.state.choices);if(!n)for(var r in a.state.choices.slice()){var o=a.state.choices[r];if(o.selected&&o.hash!=t){o.selected=!1;break}}return a.state.choices.map(function(e){return Object.assign({},e,{selected:e.hash==t||e.selected})})},a.deselectChoice=function(e,t){return a.state.choices.map(function(e){return Object.assign({},e,{selected:e.hash!=t&&e.selected})})},a.state={isLoading:!1,choices:e.poll.choices,choicesLeft:(0,h.getChoicesLeft)(e.poll,e.poll.choices)},a}return s(t,e),i(t,[{key:"clean",value:function(){return this.state.choicesLeft!==this.props.poll.allowed_choices||(P["default"].error(gettext("You need to select at least one choice")),!1)}},{key:"send",value:function(){var e=[];for(var t in this.state.choices.slice()){var a=this.state.choices[t];a.selected&&e.push(a.hash)}return x["default"].post(this.props.poll.api.votes,e)}},{key:"handleSuccess",value:function(e){M["default"].dispatch(k.replace(e)),P["default"].success(gettext("Your vote has been saved.")),this.props.showResults()}},{key:"handleError",value:function(e){400===e.status?P["default"].error(e.detail):P["default"].apiError(e)}},{key:"render",value:function(){var e=[];return this.props.poll.acl.can_vote&&e.push(0),(this.props.poll.is_public||this.props.poll.acl.can_see_votes)&&e.push(1),this.props.poll.acl.can_edit&&e.push(2),this.props.poll.acl.can_delete&&e.push(3),c["default"].createElement("div",{className:"panel panel-default panel-poll"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"panel-body"},c["default"].createElement("h2",null,this.props.poll.question),c["default"].createElement(v["default"],{poll:this.props.poll}),c["default"].createElement(m["default"],{choices:this.state.choices,toggleChoice:this.toggleChoice}),c["default"].createElement(f["default"],{choicesLeft:this.state.choicesLeft,poll:this.props.poll})),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:(0,_.getClassName)(e,0)},c["default"].createElement(y["default"],{className:"btn-primary btn-block btn-sm",loading:this.state.isLoading},gettext("Save your vote"))),c["default"].createElement("div",{className:(0,_.getClassName)(e,1)},c["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.state.isLoading,onClick:this.props.showResults,type:"button"},gettext("See results"))),c["default"].createElement(_.Edit,{controls:e,poll:this.props.poll,thread:this.props.thread}),c["default"].createElement(_.Delete,{controls:e,poll:this.props.poll})))))}}]),t}(w["default"]);a["default"]=S},{"../../../reducers/poll":348,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,"../../button":7,"../../form":54,"../info":104,"../results/options":109,"./help":110,"./select":112,"./utils":113,react:"react"}],112:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.ChoiceSelect=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return u["default"].createElement("ul",{className:"list-unstyled poll-select-choices"},e.choices.map(function(t){return u["default"].createElement(c,{choice:t,key:t.hash,toggleChoice:e.toggleChoice})}))};var i=e("react"),u=n(i),c=a.ChoiceSelect=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.toggleChoice(n.props.choice.hash)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("li",{className:"poll-select-choice"},u["default"].createElement("button",{className:this.props.choice.selected?"btn btn-selected":"btn",onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},this.props.choice.selected?"check_box":"check_box_outline_blank"),u["default"].createElement("strong",null,this.props.choice.label)))}}]),t}(u["default"].Component)},{react:"react"}],113:[function(e,t,a){"use strict";function n(e,t){for(var a in e){var n=e[a];if(n.hash===t)return n}return null}function r(e,t){var a=[];for(var n in t){var r=t[n];r.selected&&a.push(r)}return e.allowed_choices-a.length}Object.defineProperty(a,"__esModule",{value:!0}),a.getChoiceFromHash=n,a.getChoicesLeft=r},{}],114:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return"?"===e.item[0]?null:i["default"].createElement("li",{className:o(e.item)},l(e.item))}function o(e){var t="diff-item";return"-"===e[0]?t+=" diff-item-sub":"+"===e[0]&&(t+=" diff-item-add"),t}function l(e){return e.substr(2)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return i["default"].createElement("div",{className:"modal-body post-changelog-diff"},i["default"].createElement("ul",{className:"list-unstyled"},e.diff.map(function(e,t){return i["default"].createElement(r,{item:e,key:t})})))},a.DiffItem=r,a.getItemClassName=o,a.cleanItem=l;var s=e("react"),i=n(s)},{react:"react"}],115:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.revertEdit(n.props.edit.id)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.canRevert?u["default"].createElement("div",{className:"modal-footer visible-xs-block"},u["default"].createElement(d["default"],{className:"btn-default btn-sm btn-block",disabled:this.props.disabled,onClick:this.onClick,title:gettext("Revert post to state from before this edit.")},gettext("Revert"))):null}}]),t}(u["default"].Component);a["default"]=f},{"../button":7,react:"react"}],116:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:e.className||"modal-dialog",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Post edits history"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalDialog=i;var c=e("react"),d=r(c),f=e("./diff"),p=r(f),m=e("./footer"),h=r(m),b=e("./toolbar"),v=r(b),_=e("./utils"),g=e("../modal-message"),y=r(g),E=e("../modal-loader"),w=r(E),O=e("../../reducers/post"),k=n(O),N=e("../../services/ajax"),x=r(N),j=e("../../services/modal"),P=r(j),C=e("../../services/snackbar"),M=r(C),S=e("../../services/store"),T=r(S),L=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.goToEdit=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;a.setState({isBusy:!0});var t=a.props.post.api.edits;null!==e&&(t+="?edit="+e),x["default"].get(t).then(function(e){a.setState({isReady:!0,isBusy:!1,edit:(0,_.hydrateEdit)(e)})},function(e){a.setState({isReady:!0,isBusy:!1,error:e.detail})})},a.revertEdit=function(e){if(!a.state.isBusy){var t=confirm(gettext("Are you sure you with to revert this post to the state from before this edit?"));if(t){a.setState({isBusy:!0});var n=a.props.post.api.edits+"?edit="+e;x["default"].post(n).then(function(e){var t=k.hydrate(e);T["default"].dispatch(k.patch(e,t)),M["default"].success(gettext("Post has been reverted to previous state.")),P["default"].hide()},function(e){M["default"].apiError(e),a.setState({isBusy:!1})})}}},a.state={isReady:!1,isBusy:!0,canRevert:e.post.acl.can_edit,error:null,edit:null},a}return s(t,e),u(t,[{key:"componentDidMount",value:function(){this.goToEdit()}},{key:"render",value:function(){return this.state.error?d["default"].createElement(i,{className:"modal-dialog modal-message"},d["default"].createElement(y["default"],{message:this.state.error})):this.state.isReady?d["default"].createElement(i,null,d["default"].createElement(v["default"],{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,goToEdit:this.goToEdit,revertEdit:this.revertEdit}),d["default"].createElement(p["default"],{diff:this.state.edit.diff}),d["default"].createElement(h["default"],{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,revertEdit:this.revertEdit})):d["default"].createElement(i,null,d["default"].createElement(w["default"],null))}}]),t}(d["default"].Component);a["default"]=L},{"../../reducers/post":349,"../../services/ajax":361,"../../services/modal":367,"../../services/snackbar":372,"../../services/store":373,"../modal-loader":59,"../modal-message":60,"./diff":114,"./footer":115,"./toolbar":117,"./utils":118,react:"react"}],117:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.previous,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function i(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function u(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"last_page"))}function c(e){return e.canRevert?m["default"].createElement("div",{className:"col-sm-3 hidden-xs"},m["default"].createElement(b["default"],{className:"btn-default btn-sm btn-block",disabled:e.disabled,onClick:e.onClick,title:gettext("Revert post to state from before this edit.")},gettext("Revert"))):null}function d(e){var t=null;t=e.edit.url.editor?interpolate(E,{url:(0,_["default"])(e.edit.url.editor),user:(0,_["default"])(e.edit.editor_name)},!0):interpolate(y,{user:(0,_["default"])(e.edit.editor_name)},!0);var a=interpolate(g,{absolute:(0,_["default"])(e.edit.edited_on.format("LLL")),relative:(0,_["default"])(e.edit.edited_on.fromNow())},!0),n=interpolate((0,_["default"])(gettext("By %(edited_by)s %(edited_on)s.")),{edited_by:t,edited_on:a},!0);return m["default"].createElement("p",{dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0});var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.GoBackBtn=s,a.GoForwardBtn=i,a.GoLastBtn=u,a.RevertBtn=c,a.Label=d;var p=e("react"),m=n(p),h=e("../button"),b=n(h),v=e("../../utils/escape-html"),_=n(v),g='<abbr title="%(absolute)s">%(relative)s</abbr>',y='<span class="item-title">%(user)s</span>',E='<a href="%(url)s" class="item-title">%(user)s</a>',w=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.goLast=function(){n.props.goToEdit()},n.goForward=function(){n.props.goToEdit(n.props.edit.next)},n.goBack=function(){n.props.goToEdit(n.props.edit.previous)},n.revertEdit=function(){n.props.revertEdit(n.props.edit.id)},l=a,o(n,l)}return l(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("div",{className:"modal-toolbar post-changelog-toolbar"},m["default"].createElement("div",{className:"row"},m["default"].createElement("div",{className:"col-xs-12 col-sm-4"},m["default"].createElement("div",{className:"row"},m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(s,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goBack})),m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(i,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goForward})),m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(u,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goLast})))),m["default"].createElement("div",{className:"col-xs-12 col-sm-5 xs-margin-top-half post-change-label"},m["default"].createElement(d,{edit:this.props.edit})),m["default"].createElement(c,{canRevert:this.props.canRevert,disabled:this.props.disabled,onClick:this.revertEdit})))}}]),t}(m["default"].Component);a["default"]=w},{"../../utils/escape-html":379,"../button":7,react:"react"}],118:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{edited_on:(0,l["default"])(e.edited_on)})}Object.defineProperty(a,"__esModule",{value:!0}),a.hydrateEdit=r;var o=e("moment"),l=n(o)},{moment:"moment"}],119:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.isReady,a=e.posts,n=e.poster;return t?o["default"].createElement("ul",{className:"posts-list post-feed ui-ready"},a.map(function(e){return o["default"].createElement(s["default"],{key:e.id,post:e,poster:n})})):o["default"].createElement(u["default"],null)};var r=e("react"),o=n(r),l=e("./post"),s=n(l),i=e("./preview"),u=n(i)},{"./post":122,"./preview":128,react:"react"}],120:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("div",{className:"post-body"},s["default"].createElement(u["default"],{markup:e.post.content}))}function o(e){return s["default"].createElement("div",{className:"post-body post-body-invalid"},s["default"].createElement("p",{className:"lead"},gettext("This post's contents cannot be displayed.")),s["default"].createElement("p",{className:"text-muted"},gettext("This error is caused by invalid post content manipulation.")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.content?s["default"].createElement(r,e):s["default"].createElement(o,e)},a.Default=r,a.Invalid=o;var l=e("react"),s=n(l),i=e("../../misago-markup"),u=n(i),c=e("../../../utils/escape-html");n(c)},{"../../../utils/escape-html":379,"../../misago-markup":58,react:"react"}],121:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=t.category,n=t.thread,r=interpolate(gettext("posted %(posted_on)s"),{posted_on:t.posted_on.format("LL, LT")},!0);return o["default"].createElement("div",{className:"post-heading"},o["default"].createElement("a",{className:"btn btn-link item-title",href:n.url},n.title),o["default"].createElement("a",{className:"btn btn-link post-category",href:a.url.index},a.name),o["default"].createElement("a",{href:t.url.index,className:"btn btn-link posted-on",title:r},t.posted_on.fromNow()))};var r=e("react"),o=n(r)},{react:"react"}],122:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster,n=a||t.poster,r="post";return n&&n.rank.css_class&&(r+=" post-"+n.rank.css_class),o["default"].createElement("li",{className:r,id:"post-"+t.id},o["default"].createElement("div",{className:"panel panel-default panel-post"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement(d["default"],{post:t,poster:n}),o["default"].createElement(u["default"],{post:t}),o["default"].createElement(s["default"],{post:t}))))};var r=e("react"),o=n(r),l=e("./body"),s=n(l),i=e("./header"),u=n(i),c=e("./post-side"),d=n(c)},{"./body":120,"./header":121,"./post-side":125,react:"react"}],123:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return o["default"].createElement("div",{className:"post-side post-side-anonymous"},o["default"].createElement(u["default"],{post:t}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("span",null,o["default"].createElement(s["default"],{className:"poster-avatar",size:50}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("span",{className:"item-title"},t.poster_name)),o["default"].createElement("span",{className:"user-title user-title-anonymous"},gettext("Removed user")))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("./button"),u=n(i)},{"../../../avatar":5,"./button":124,react:"react"}],124:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return o["default"].createElement("a",{className:"btn btn-default btn-icon pull-right",href:t.url.index},o["default"].createElement("span",{className:"btn-text-left hidden-xs"},gettext("See post")),o["default"].createElement("span",{className:"material-icon"},"chevron_right"))};var r=e("react"),o=n(r)},{react:"react"}],125:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster;return a.id?o["default"].createElement(u["default"],{post:t,poster:a}):o["default"].createElement(s["default"],{post:t})};var r=e("react"),o=n(r),l=e("./anonymous"),s=n(l),i=e("./registered"),u=n(i)},{"./anonymous":123,"./registered":126,react:"react"}],126:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster;return o["default"].createElement("div",{className:"post-side post-side-registered"},o["default"].createElement(u["default"],{post:t}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"
-},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{className:"poster-avatar",size:50,user:a}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("a",{className:"item-title",href:a.url},a.username)),o["default"].createElement(d["default"],{title:a.title,rank:a.rank}))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("./button"),u=n(i),c=e("./user-title"),d=n(c)},{"../../../avatar":5,"./button":124,"./user-title":127,react:"react"}],127:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.rank,a=e.title,n=a||t.title||t.name,r="user-title";return t.css_class&&(r+=" user-title-"+t.css_class),t.is_tab?o["default"].createElement("a",{className:r,href:t.url},n):o["default"].createElement("span",{className:r},n)};var r=e("react"),o=n(r)},{react:"react"}],128:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return l["default"].createElement("ul",{className:"posts-list post-feed ui-preview"},l["default"].createElement("li",{className:"post"},l["default"].createElement("div",{className:"panel panel-default panel-post"},l["default"].createElement("div",{className:"panel-body"},l["default"].createElement("div",{className:"post-side post-side-anonymous"},l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left"},l["default"].createElement("span",null,l["default"].createElement(i["default"],{className:"poster-avatar",size:50}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement("div",{className:"media-heading"},l["default"].createElement("span",{className:"item-title"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))),l["default"].createElement("span",{className:"user-title user-title-anonymous"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))))),l["default"].createElement("div",{className:"post-heading"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")),l["default"].createElement("div",{className:"post-body"},l["default"].createElement("article",{className:"misago-markup"},l["default"].createElement("p",null,l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")," ",l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")," ",l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))))))))};var o=e("react"),l=r(o),s=e("../avatar"),i=r(s),u=e("../../utils/random"),c=n(u)},{"../../utils/random":384,"../avatar":5,react:"react"}],129:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return Object.assign({},e,{liked_on:(0,v["default"])(e.liked_on)})}function i(e){var t=e.className,a=e.children,n=e.likes,r=gettext("Post Likes");if(n){var o=n.length,l=ngettext("%(likes)s like","%(likes)s likes",o);r=interpolate(l,{likes:o},!0)}return h["default"].createElement("div",{className:"modal-dialog "+(t||""),role:"document"},h["default"].createElement("div",{className:"modal-content"},h["default"].createElement("div",{className:"modal-header"},h["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},h["default"].createElement("span",{"aria-hidden":"true"},"×")),h["default"].createElement("h4",{className:"modal-title"},r)),a))}function u(e){return h["default"].createElement("div",{className:"modal-body modal-post-likers"},h["default"].createElement("ul",{className:"media-list"},e.likes.map(function(e){return h["default"].createElement(c,f({key:e.id},e))})))}function c(e){if(e.url){var t={id:e.liker_id,avatars:e.avatars};return h["default"].createElement("li",{className:"media"},h["default"].createElement("div",{className:"media-left"},h["default"].createElement("a",{className:"user-avatar",href:e.url},h["default"].createElement(g["default"],{size:"50",user:t}))),h["default"].createElement("div",{className:"media-body"},h["default"].createElement("a",{className:"item-title",href:e.url},e.username)," ",h["default"].createElement(d,{likedOn:e.liked_on})))}return h["default"].createElement("li",{className:"media"},h["default"].createElement("div",{className:"media-left"},h["default"].createElement("span",{className:"user-avatar"},h["default"].createElement(g["default"],{size:"50"}))),h["default"].createElement("div",{className:"media-body"},h["default"].createElement("strong",null,e.username)," ",h["default"].createElement(d,{likedOn:e.liked_on})))}function d(e){return h["default"].createElement("span",{className:"text-muted",title:e.likedOn.format("LLL")},e.likedOn.fromNow())}Object.defineProperty(a,"__esModule",{value:!0});var f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},p=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.hydrateLike=s,a.ModalDialog=i,a.LikesList=u,a.LikeDetails=c,a.LikeDate=d;var m=e("react"),h=n(m),b=e("moment"),v=n(b),_=e("./avatar"),g=n(_),y=e("./modal-message"),E=n(y),w=e("./modal-loader"),O=n(w),k=e("../services/ajax"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isReady:!1,error:null,likes:[]},a}return l(t,e),p(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.post.api.likes).then(function(t){e.setState({isReady:!0,likes:t.map(s)})},function(t){e.setState({isReady:!0,error:t.detail})})}},{key:"render",value:function(){return this.state.error?h["default"].createElement(i,{className:"modal-message"},h["default"].createElement(E["default"],{message:this.state.error})):this.state.isReady?this.state.likes.length?h["default"].createElement(i,{className:"modal-sm",likes:this.state.likes},h["default"].createElement(u,{likes:this.state.likes})):h["default"].createElement(i,{className:"modal-message"},h["default"].createElement(E["default"],{message:gettext("No users have liked this post.")})):h["default"].createElement(i,{className:"modal-sm"},h["default"].createElement(O["default"],null))}}]),t}(h["default"].Component);a["default"]=x},{"../services/ajax":361,"./avatar":5,"./modal-loader":59,"./modal-message":60,moment:"moment",react:"react"}],130:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/loader"),_=r(v),g=e("./utils/message"),y=r(g),E=e("./utils/attachments"),w=n(E),O=e("./utils/validators"),k=e("../../services/ajax"),N=r(k),x=e("../../services/posting"),j=r(x),P=e("../../services/snackbar"),C=r(P),M=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){a.setState({isReady:!0,post:e.post,attachments:w.hydrate(e.attachments),protect:e.is_protected,canProtect:e.can_protect})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard changes?"));e&&j["default"].close()},a.onProtect=function(){a.setState({protect:!0})},a.onUnprotect=function(){a.setState({protect:!1})},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})},a.state={isReady:!1,isLoading:!1,isErrored:!1,post:"",attachments:[],protect:!1,canProtect:!1,validators:{post:(0,O.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){N["default"].get(this.props.config).then(this.loadSuccess,this.loadError)}},{key:"clean",value:function(){if(!this.state.post.trim().length)return C["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return!e.post||(C["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return N["default"].put(this.props.submit,{post:this.state.post,attachments:w.clean(this.state.attachments),protect:this.state.protect})}},{key:"handleSuccess",value:function(e){C["default"].success(gettext("Reply has been edited.")),window.location=e.url.index,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);C["default"].error(t[0])}else C["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?c["default"].createElement(b["default"],{className:"posting-form"},c["default"].createElement("form",{onSubmit:this.handleSubmit,method:"POST"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,canProtect:this.state.canProtect,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,onProtect:this.onProtect,onUnprotect:this.onUnprotect,protect:this.state.protect,submitLabel:gettext("Edit reply"),value:this.state.post}))))):this.state.isErrored?c["default"].createElement(y["default"],{message:this.state.isErrored}):c["default"].createElement(_["default"],null)}}]),t}(m["default"]);a["default"]=M},{"../../services/ajax":361,"../../services/posting":371,"../../services/snackbar":372,"../editor":50,"../form":54,"./utils/attachments":135,"./utils/container":136,"./utils/loader":137,"./utils/message":138,"./utils/validators":141,react:"react"}],131:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return"START"===e.mode?o["default"].createElement(s["default"],e):"START_PRIVATE"===e.mode?o["default"].createElement(u["default"],e):"REPLY"===e.mode?o["default"].createElement(d["default"],e):"EDIT"===e.mode?o["default"].createElement(p["default"],e):null};var r=e("react"),o=n(r),l=e("./start"),s=n(l),i=e("./start-private"),u=n(i),c=e("./reply"),d=n(c),f=e("./edit"),p=n(f)},{"./edit":130,"./reply":132,"./start":134,"./start-private":133,react:"react"}],132:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/loader"),_=r(v),g=e("./utils/message"),y=r(g),E=e("./utils/attachments"),w=n(E),O=e("./utils/validators"),k=e("../../services/ajax"),N=r(k),x=e("../../services/posting"),j=r(x),P=e("../../services/snackbar"),C=r(P),M=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){a.setState({isReady:!0,post:e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]":""})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.appendData=function(e){var t=e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]\n\n":"";a.setState(function(e,a){return e.post.length>0?{post:e.post+"\n\n"+t}:{post:t}})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard your reply?"));e&&j["default"].close()},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})},a.state={isReady:!1,isLoading:!1,isErrored:!1,post:"",attachments:[],validators:{post:(0,O.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){N["default"].get(this.props.config,this.props.context||null).then(this.loadSuccess,this.loadError)}},{key:"componentWillReceiveProps",value:function(e){var t=this.props.context,a=e.context;t&&a&&t.reply===a.reply||N["default"].get(e.config,e.context||null).then(this.appendData,C["default"].apiError)}},{key:"clean",value:function(){if(!this.state.post.trim().length)return C["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return!e.post||(C["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return N["default"].post(this.props.submit,{post:this.state.post,attachments:w.clean(this.state.attachments)})}},{key:"handleSuccess",value:function(e){C["default"].success(gettext("Your reply has been posted.")),window.location=e.url.index,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.post||[],e.attachments||[]);C["default"].error(t[0])}else C["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?c["default"].createElement(b["default"],{className:"posting-form"},c["default"].createElement("form",{onSubmit:this.handleSubmit,method:"POST"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post reply"),value:this.state.post}))))):this.state.isErrored?c["default"].createElement(y["default"],{message:this.state.isErrored}):c["default"].createElement(_["default"],null)}}]),t}(m["default"]);a["default"]=M},{"../../services/ajax":361,"../../services/posting":371,"../../services/snackbar":372,"../editor":50,"../form":54,"./utils/attachments":135,"./utils/container":136,"./utils/loader":137,"./utils/message":138,"./utils/validators":141,react:"react"}],133:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/message"),_=(r(v),e("./utils/attachments")),g=n(_),y=e("./utils/usernames"),E=r(y),w=e("./utils/validators"),O=e("../../services/ajax"),k=r(O),N=e("../../services/posting"),x=r(N),j=e("../../services/snackbar"),P=r(j),C=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard private thread?"));e&&x["default"].close()},a.onToChange=function(e){a.changeValue("to",e.target.value)},a.onTitleChange=function(e){a.changeValue("title",e.target.value)},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})};var n=(e.to||[]).map(function(e){return e.username}).join(", ");return a.state={isLoading:!1,to:n,title:"",post:"",attachments:[],validators:{title:(0,w.getTitleValidators)(),post:(0,w.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"clean",value:function(){if(!(0,E["default"])(this.state.to).length)return P["default"].error(gettext("You have to enter at least one recipient.")),!1;if(!this.state.title.trim().length)return P["default"].error(gettext("You have to enter thread title.")),!1;if(!this.state.post.trim().length)return P["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return e.title?(P["default"].error(e.title[0]),!1):!e.post||(P["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.submit,{to:(0,E["default"])(this.state.to),title:this.state.title,post:this.state.post,attachments:g.clean(this.state.attachments)})}},{key:"handleSuccess",value:function(e){P["default"].success(gettext("Your thread has been posted.")),window.location=e.url,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.to||[],e.title||[],e.post||[],e.attachments||[]);P["default"].error(t[0])}else P["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement(b["default"],{className:"posting-form",withFirstRow:!0},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onToChange,placeholder:gettext("Comma separated list of user names, eg.: Danny, Lisa"),type:"text",value:this.state.to}))),c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:gettext("Thread title"),type:"text",value:this.state.title}))),c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post thread"),value:this.state.post})))))}}]),t}(m["default"]);a["default"]=C},{"../../services/ajax":361,"../../services/posting":371,"../../services/snackbar":372,"../editor":50,"../form":54,"./utils/attachments":135,"./utils/container":136,"./utils/message":138,"./utils/usernames":140,"./utils/validators":141,react:"react"}],134:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../category-select"),f=r(d),p=e("../editor"),m=r(p),h=e("../form"),b=r(h),v=e("./utils/container"),_=r(v),g=e("./utils/loader"),y=r(g),E=e("./utils/message"),w=r(E),O=e("./utils/options"),k=r(O),N=e("./utils/attachments"),x=n(N),j=e("./utils/validators"),P=e("../../services/ajax"),C=r(P),M=e("../../services/posting"),S=r(M),T=e("../../services/snackbar"),L=r(T),A=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){var t=null,n=!1,r=null,o=e.map(function(e){return e.post===!1||t&&e.id!=a.state.category||(t=e.id,r=e.post),e.post&&(e.post.close||e.post.hide||e.post.pin)&&(n=!0),Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id})});a.setState({isReady:!0,showOptions:n,categories:o,category:t,categoryOptions:r})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard thread?"));e&&S["default"].close()},a.onTitleChange=function(e){a.changeValue("title",e.target.value)},a.onCategoryChange=function(e){var t=a.state.categories.find(function(t){return e.target.value==t.value}),n=a.state.pin;t.post.pin&&t.post.pin<n&&(n=t.post.pin),a.setState({category:t.id,categoryOptions:t.post,pin:n})},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})},a.onClose=function(){a.changeValue("close",!0)},a.onOpen=function(){a.changeValue("close",!1)},a.onPinGlobally=function(){a.changeValue("pin",2)},a.onPinLocally=function(){a.changeValue("pin",1)},a.onUnpin=function(){a.changeValue("pin",0)},a.onHide=function(){a.changeValue("hide",!0)},a.onUnhide=function(){a.changeValue("hide",!1)},a.state={isReady:!1,isLoading:!1,isErrored:!1,showOptions:!1,categoryOptions:null,title:"",category:e.category||null,categories:[],post:"",attachments:[],close:!1,hide:!1,pin:0,validators:{title:(0,j.getTitleValidators)(),post:(0,j.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){C["default"].get(this.props.config).then(this.loadSuccess,this.loadError)}},{key:"clean",value:function(){if(!this.state.title.trim().length)return L["default"].error(gettext("You have to enter thread title.")),!1;if(!this.state.post.trim().length)return L["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return e.title?(L["default"].error(e.title[0]),!1):!e.post||(L["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return C["default"].post(this.props.submit,{title:this.state.title,category:this.state.category,post:this.state.post,attachments:x.clean(this.state.attachments),close:this.state.close,hide:this.state.hide,pin:this.state.pin})}},{key:"handleSuccess",value:function(e){L["default"].success(gettext("Your thread has been posted.")),window.location=e.url,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);L["default"].error(t[0])}else L["default"].apiError(e)}},{key:"render",value:function(){if(this.state.isErrored)return c["default"].createElement(w["default"],{message:this.state.isErrored});if(!this.state.isReady)return c["default"].createElement(y["default"],null);var e=0;this.state.categoryOptions.close&&(e+=1),this.state.categoryOptions.hide&&(e+=1),this.state.categoryOptions.pin&&(e+=1);var t=null;return t=1===e?"col-sm-6":"col-sm-8",t+=3===e?" col-md-6":e?" col-md-7":" col-md-9",c["default"].createElement(_["default"],{className:"posting-form",withFirstRow:!0},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:t},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:gettext("Thread title"),type:"text",value:this.state.title})),c["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3 xs-margin-top"},c["default"].createElement(f["default"],{choices:this.state.categories,disabled:this.state.isLoading,onChange:this.onCategoryChange,value:this.state.category})),c["default"].createElement(k["default"],{close:this.state.close,columns:e,disabled:this.state.isLoading,hide:this.state.hide,onClose:this.onClose,onHide:this.onHide,onOpen:this.onOpen,onPinGlobally:this.onPinGlobally,onPinLocally:this.onPinLocally,onUnhide:this.onUnhide,onUnpin:this.onUnpin,options:this.state.categoryOptions,pin:this.state.pin,showOptions:this.state.showOptions})),c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(m["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post thread"),value:this.state.post})))))}}]),t}(b["default"]);a["default"]=A},{"../../services/ajax":361,"../../services/posting":371,"../../services/snackbar":372,"../category-select":20,"../editor":50,"../form":54,"./utils/attachments":135,"./utils/container":136,"./utils/loader":137,"./utils/message":138,"./utils/options":139,"./utils/validators":141,react:"react"}],135:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.filter(function(e){return e.id&&!e.isRemoved});return t.map(function(e){return e.id})}function o(e){return e.map(function(e){return Object.assign({},e,{uploaded_on:(0,s["default"])(e.uploaded_on)})})}Object.defineProperty(a,"__esModule",{value:!0}),a.clean=r,a.hydrate=o;var l=e("moment"),s=n(l)},{moment:"moment"}],136:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:e.className},o["default"].createElement("div",{className:"container"},e.children))};var r=e("react"),o=n(r)},{react:"react"}],137:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement(s["default"],{className:"posting-loader"},o["default"].createElement(u["default"],null))};var r=e("react"),o=n(r),l=e("./container"),s=n(l),i=e("../../loader"),u=n(i)},{"../../loader":56,"./container":136,react:"react"}],138:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement(s["default"],{className:"posting-message"},o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",null,o["default"].createElement("span",{className:"material-icon"},"error_outline"),e.message),o["default"].createElement("button",{type:"button",className:"btn btn-default",onClick:u["default"].close},gettext("Dismiss"))))};var r=e("react"),o=n(r),l=e("./container"),s=n(l),i=e("../../../services/posting"),u=n(i)},{"../../../services/posting":371,"./container":136,react:"react"}],139:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(!e.show)return null;var t=e.close?gettext("Closed"):gettext("Open");return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:e.close?e.onOpen:e.onClose,title:t,type:"button"},i["default"].createElement("span",{className:"material-icon"},e.close?"lock":"lock_outline"),i["default"].createElement("span",{className:e.textClassName},t)))}function o(e){if(!e.show)return null;var t=e.hide?gettext("Hidden"):gettext("Not hidden");return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:e.hide?e.onUnhide:e.onHide,title:t,type:"button"},i["default"].createElement("span",{className:"material-icon"},e.hide?"visibility_off":"visibility"),i["default"].createElement("span",{className:e.textClassName},t)))}function l(e){if(!e.show)return null;var t=null,a=null,n=null;switch(e.pin){case 0:t="radio_button_unchecked",a=e.onPinLocally,n=gettext("Unpinned");break;case 1:t="bookmark_outline",a=e.onPinGlobally,n=gettext("Pinned locally"),a=2==e.show?e.onPinGlobally:e.onUnpin;break;case 2:t="bookmark",a=e.onUnpin,n=gettext("Pinned globally")}return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:a,title:n,type:"button"},i["default"].createElement("span",{className:"material-icon"},t),i["default"].createElement("span",{className:e.textClassName},n)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){if(!e.showOptions)return null;var t=e.columns,a="col-xs-12 xs-margin-top";a+=1===t?" col-sm-2":" sm-margin-top",a+=3===t?" col-md-3":" col-md-2",a+=" posting-options";var n="col-xs-"+12/t,s="btn-text";return s+=3===t?" visible-sm-inline-block":2===t?" hidden-md hidden-lg":" hidden-sm",i["default"].createElement("div",{
-className:a},i["default"].createElement("div",{className:"row"},i["default"].createElement(l,{className:n,disabled:e.disabled,onPinGlobally:e.onPinGlobally,onPinLocally:e.onPinLocally,onUnpin:e.onUnpin,pin:e.pin,show:e.options.pin,textClassName:s}),i["default"].createElement(o,{className:n,disabled:e.disabled,hide:e.hide,onHide:e.onHide,onUnhide:e.onUnhide,show:e.options.hide,textClassName:s}),i["default"].createElement(r,{className:n,close:e.close,disabled:e.disabled,onClose:e.onClose,onOpen:e.onOpen,show:e.options.close,textClassName:s})))},a.CloseOptions=r,a.HideOptions=o,a.PinOptions=l;var s=e("react"),i=n(s)},{react:"react"}],140:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.split(",").map(function(e){return e.trim().toLowerCase()}),a=t.filter(function(e){return e.length>0}),n=a.filter(function(e,t){return a.indexOf(e)==t});return n}},{}],141:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){return[l(),s()]}function o(){return f["default"].get("SETTINGS").post_length_max?[i(),u()]:[i()]}function l(){return(0,c.minLength)(f["default"].get("SETTINGS").thread_title_length_min,function(e,t){var a=ngettext("Thread title should be at least %(limit_value)s character long (it has %(show_value)s).","Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function s(){return(0,c.maxLength)(f["default"].get("SETTINGS").thread_title_length_max,function(e,t){var a=ngettext("Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).","Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function i(){return(0,c.minLength)(f["default"].get("SETTINGS").post_length_min,function(e,t){var a=ngettext("Posted message should be at least %(limit_value)s character long (it has %(show_value)s).","Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function u(){return(0,c.maxLength)(f["default"].get("SETTINGS").post_length_max||1e6,function(e,t){var a=ngettext("Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).","Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}Object.defineProperty(a,"__esModule",{value:!0}),a.getTitleValidators=r,a.getPostValidators=o,a.getTitleLengthMin=l,a.getTitleLengthMax=s,a.validatePostLengthMin=i,a.validatePostLengthMax=u;var c=e("../../../utils/validators"),d=e("../../.."),f=n(d)},{"../../..":299,"../../../utils/validators":389}],142:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.can_hide}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=void 0;var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return i(e.post.acl)?d["default"].createElement("li",{className:"event-controls"},d["default"].createElement(w,e),d["default"].createElement(O,e),d["default"].createElement(k,e)):null},a.isVisible=i;var c=e("react"),d=r(c),f=e("moment"),p=r(f),m=e("../../../reducers/post"),h=n(m),b=e("../../../services/ajax"),v=r(b),_=e("../../../services/snackbar"),g=r(_),y=e("../../../services/store"),E=r(y),w=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].dispatch(h.patch(n.props.post,{is_hidden:!0,hidden_on:(0,p["default"])(),hidden_by_name:n.props.user.username,url:Object.assign(n.props.post.url,{hidden_by:n.props.user.url})}));var e={op:"replace",path:"is-hidden",value:!0};v["default"].patch(n.props.post.api.index,[e]).then(function(e){E["default"].dispatch(h.patch(n.props.post,e))},function(e){400===e.status?g["default"].error(e.detail[0]):g["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{is_hidden:!1}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.post.is_hidden?null:d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Hide"))}}]),t}(d["default"].Component),O=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].dispatch(h.patch(n.props.post,{is_hidden:!1}));var e={op:"replace",path:"is-hidden",value:!1};v["default"].patch(n.props.post.api.index,[e]).then(function(e){E["default"].dispatch(h.patch(n.props.post,e))},function(e){400===e.status?g["default"].error(e.detail[0]):g["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{is_hidden:!0}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.post.is_hidden?d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Unhide")):null}}]),t}(d["default"].Component),k=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=confirm(gettext("Are you sure you wish to delete this event? This action is not reversible!"));e&&n["delete"]()},n["delete"]=function(){E["default"].dispatch(h.patch(n.props.post,{isDeleted:!0})),v["default"]["delete"](n.props.post.api.index).then(function(){g["default"].success(gettext("Event has been deleted."))},function(e){400===e.status?g["default"].error(e.detail[0]):g["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{isDeleted:!1}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Delete"))}}]),t}(d["default"].Component)},{"../../../reducers/post":349,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,moment:"moment",react:"react"}],143:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"post-avatar"},o["default"].createElement("span",{className:"material-icon"},l[e.post.event_type]))};var r=e("react"),o=n(r),l={changed_title:"edit",pinned_globally:"bookmark",pinned_locally:"bookmark_border",unpinned:"panorama_fish_eye",moved:"arrow_forward",merged:"call_merge",approved:"done",opened:"lock_open",closed:"lock_outline",unhid:"visibility",hid:"visibility_off",changed_owner:"grade",tookover:"grade",added_participant:"person_add",owner_left:"person_outline",participant_left:"person_outline",removed_participant:"remove_circle_outline"}},{react:"react"}],144:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t="event";return e.post.isDeleted?t="hide":e.post.is_hidden&&(t="event post-hidden"),o["default"].createElement("li",{id:"post-"+e.post.id,className:t},o["default"].createElement(p["default"],{post:e.post}),o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-2 col-sm-3 text-right"},o["default"].createElement(s["default"],e)),o["default"].createElement("div",{className:"col-xs-10 col-sm-9 text-left"},o["default"].createElement(h["default"],{post:e.post},o["default"].createElement(d["default"],e),o["default"].createElement(u["default"],e)))))};var r=e("react"),o=n(r),l=e("./icon"),s=n(l),i=e("./info"),u=n(i),c=e("./message"),d=n(c),f=e("./unread-label"),p=n(f),m=e("../waypoint"),h=n(m)},{"../waypoint":170,"./icon":143,"./info":145,"./message":146,"./unread-label":147,react:"react"}],145:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(e.post.is_hidden){var t=null;t=e.post.url.hidden_by?interpolate(b,{url:(0,c["default"])(e.post.url.hidden_by),user:(0,c["default"])(e.post.hidden_by_name)},!0):interpolate(h,{user:(0,c["default"])(e.post.hidden_by_name)},!0);var a=interpolate(p,{absolute:(0,c["default"])(e.post.hidden_on.format("LLL")),relative:(0,c["default"])(e.post.hidden_on.fromNow())},!0),n=interpolate((0,c["default"])(gettext("Hidden by %(event_by)s %(event_on)s.")),{event_by:t,event_on:a},!0);return i["default"].createElement("li",{className:"event-hidden-message",dangerouslySetInnerHTML:{__html:n}})}return null}function o(e){var t=null;t=e.post.poster?interpolate(b,{url:(0,c["default"])(e.post.poster.url),user:(0,c["default"])(e.post.poster_name)},!0):interpolate(h,{user:(0,c["default"])(e.post.poster_name)},!0);var a=interpolate(m,{url:(0,c["default"])(e.post.url.index),absolute:(0,c["default"])(e.post.posted_on.format("LLL")),relative:(0,c["default"])(e.post.posted_on.fromNow())},!0),n=interpolate((0,c["default"])(gettext("By %(event_by)s %(event_on)s.")),{event_by:t,event_on:a},!0);return i["default"].createElement("li",{className:"event-posters",dangerouslySetInnerHTML:{__html:n}})}function l(e){return e.user.acl.can_see_users_ips?i["default"].createElement("li",{className:"event-ip"},i["default"].createElement("abbr",{title:e.post.poster_ip},gettext("IP recorded"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return i["default"].createElement("ul",{className:"list-inline event-info"},i["default"].createElement(r,e),i["default"].createElement(o,e),i["default"].createElement(l,e),i["default"].createElement(f["default"],e))},a.Hidden=r,a.Poster=o,a.Ip=l;var s=e("react"),i=n(s),u=e("../../../utils/escape-html"),c=n(u),d=e("./controls"),f=n(d),p='<abbr title="%(absolute)s">%(relative)s</abbr>',m='<a href="%(url)s" title="%(absolute)s">%(relative)s</a>',h='<span class="item-title">%(user)s</span>',b='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../../utils/escape-html":379,"./controls":142,react:"react"}],146:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=(0,p["default"])(gettext("Thread title has been changed from %(old_title)s.")),a=interpolate(b,{name:(0,p["default"])(e.post.event_context.old_title)},!0),n=interpolate(t,{old_title:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function o(e){var t=(0,p["default"])(gettext("Thread has been moved from %(from_category)s.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.from_category.url),name:(0,p["default"])(e.post.event_context.from_category.name)},!0),n=interpolate(t,{from_category:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function l(e){var t=(0,p["default"])(gettext("The %(merged_thread)s thread has been merged into this thread.")),a=interpolate(b,{name:(0,p["default"])(e.post.event_context.merged_thread)},!0),n=interpolate(t,{merged_thread:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function s(e){var t=(0,p["default"])(gettext("Changed thread owner to %(user)s.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function i(e){var t=(0,p["default"])(gettext("Added %(user)s to thread.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function u(e){var t=(0,p["default"])(gettext("Removed %(user)s from thread.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return m[e.post.event_type]?d["default"].createElement("p",{className:"event-message"},m[e.post.event_type]):"changed_title"===e.post.event_type?d["default"].createElement(r,e):"moved"===e.post.event_type?d["default"].createElement(o,e):"merged"===e.post.event_type?d["default"].createElement(l,e):"changed_owner"===e.post.event_type?d["default"].createElement(s,e):"added_participant"===e.post.event_type?d["default"].createElement(i,e):"removed_participant"===e.post.event_type?d["default"].createElement(u,e):null},a.ChangedTitle=r,a.Moved=o,a.Merged=l,a.ChangedOwner=s,a.AddedParticipant=i,a.RemovedParticipant=u;var c=e("react"),d=n(c),f=e("../../../utils/escape-html"),p=n(f),m={pinned_globally:gettext("Thread has been pinned globally."),pinned_locally:gettext("Thread has been pinned locally."),unpinned:gettext("Thread has been unpinned."),approved:gettext("Thread has been approved."),opened:gettext("Thread has been opened."),closed:gettext("Thread has been closed."),unhid:gettext("Thread has been revealed."),hid:gettext("Thread has been made hidden."),tookover:gettext("Took thread over."),owner_left:gettext("Owner has left thread. This thread is now closed."),participant_left:gettext("Participant has left thread.")},h='<a href="%(url)s" class="item-title">%(name)s</a>',b='<span class="item-title">%(name)s</span>'},{"../../../utils/escape-html":379,react:"react"}],147:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return t.is_read?null:o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-10 col-xs-offset-2 col-sm-9 col-sm-offset-3 text-left"},o["default"].createElement("div",{className:"event-label"},o["default"].createElement("span",{className:"label label-unread"},gettext("New event")))))};var r=e("react"),o=n(r)},{react:"react"}],148:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.post.is_event?s["default"].createElement(u["default"],e):s["default"].createElement(d["default"],e)}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return e.posts.isLoaded?s["default"].createElement("ul",{className:"posts-list ui-ready"},e.posts.results.map(function(t){return s["default"].createElement(r,o({key:t.id,post:t},e))})):s["default"].createElement("ul",{className:"posts-list ui-preview"},s["default"].createElement(p["default"],null))},a.ListItem=r;var l=e("react"),s=n(l),i=e("./event"),u=n(i),c=e("./post"),d=n(c),f=e("./post/preview"),p=n(f)},{"./event":144,"./post":160,"./post/preview":168,react:"react"}],149:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.attachment.is_image?u["default"].createElement("div",{className:"post-attachment-preview"},u["default"].createElement(l,e)):u["default"].createElement("div",{className:"post-attachment-preview"},u["default"].createElement(o,e))}function o(e){return u["default"].createElement("a",{href:e.attachment.url.index,className:"material-icon"},"insert_drive_file")}function l(e){var t=e.attachment.url.thumb||e.attachment.url.index;return u["default"].createElement("a",{className:"post-thumbnail",href:e.attachment.url.index,style:{backgroundImage:'url("'+(0,f["default"])(t)+'")'}})}function s(e){var t=null;t=e.attachment.url.uploader?interpolate(v,{url:(0,f["default"])(e.attachment.url.uploader),user:(0,f["default"])(e.attachment.uploader_name)},!0):interpolate(b,{user:(0,f["default"])(e.attachment.uploader_name)},!0);var a=interpolate(h,{absolute:(0,f["default"])(e.attachment.uploaded_on.format("LLL")),relative:(0,f["default"])(e.attachment.uploaded_on.fromNow())},!0),n=interpolate((0,f["default"])(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")),{filetype:e.attachment.filetype,size:(0,m["default"])(e.attachment.size),uploader:t,uploaded_on:a},!0);return u["default"].createElement("p",{className:"post-attachment-description",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return u["default"].createElement("div",{className:"col-xs-12 col-md-6"},u["default"].createElement(r,e),u["default"].createElement("div",{className:"post-attachment"},u["default"].createElement("a",{href:e.attachment.url.index,className:"attachment-name item-title"},e.attachment.filename),u["default"].createElement(s,e)))},a.AttachmentPreview=r,a.AttachmentIcon=o,a.AttachmentThumbnail=l,a.AttachmentDetails=s;var i=e("react"),u=n(i),c=e("../../../.."),d=(n(c),e("../../../../utils/escape-html")),f=n(d),p=e("../../../../utils/file-size"),m=n(p),h='<abbr title="%(absolute)s">%(relative)s</abbr>',b='<span class="item-title">%(user)s</span>',v='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../../..":299,"../../../../utils/escape-html":379,"../../../../utils/file-size":380,react:"react"}],150:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return(!e.is_hidden||e.acl.can_see_hidden)&&e.attachments}function o(e){return s["default"].createElement("div",{className:"row"},e.row.map(function(e){return s["default"].createElement(d["default"],{attachment:e,key:e?e.id:0})}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return r(e.post)?s["default"].createElement("div",{className:"post-attachments"},(0,u["default"])(e.post.attachments,2).map(function(e){var t=e.map(function(e){return e?e.id:0}).join("_");return s["default"].createElement(o,{key:t,row:e})})):null},a.isVisible=r,a.Row=o;var l=e("react"),s=n(l),i=e("../../../../utils/batch"),u=n(i),c=e("./attachment"),d=n(c)},{"../../../../utils/batch":376,"./attachment":149,react:"react"}],151:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return i["default"].createElement(c["default"],{className:"post-body",post:e.post},i["default"].createElement(f["default"],{markup:e.post.content}))}function o(e){var t=null;t=e.post.hidden_by?interpolate(h,{url:(0,m["default"])(e.post.url.hidden_by),user:(0,m["default"])(e.post.hidden_by_name)},!0):interpolate(b,{user:(0,m["default"])(e.post.hidden_by_name)},!0);var a=interpolate(v,{absolute:(0,m["default"])(e.post.hidden_on.format("LLL")),relative:(0,m["default"])(e.post.hidden_on.fromNow())},!0),n=interpolate((0,m["default"])(gettext("Hidden by %(hidden_by)s %(hidden_on)s.")),{hidden_by:t,hidden_on:a},!0);return i["default"].createElement(c["default"],{className:"post-body post-body-hidden",post:e.post},i["default"].createElement("p",{className:"lead"},gettext("This post is hidden. You cannot see its contents.")),i["default"].createElement("p",{className:"text-muted",dangerouslySetInnerHTML:{__html:n}}))}function l(e){return i["default"].createElement(c["default"],{className:"post-body post-body-invalid",post:e.post},i["default"].createElement("p",{className:"lead"},gettext("This post's contents cannot be displayed.")),i["default"].createElement("p",{className:"text-muted"},gettext("This error is caused by invalid post content manipulation.")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.is_hidden&&!e.post.acl.can_see_hidden?i["default"].createElement(o,e):e.post.content?i["default"].createElement(r,e):i["default"].createElement(l,e)},a.Default=r,a.Hidden=o,a.Invalid=l;var s=e("react"),i=n(s),u=e("../waypoint"),c=n(u),d=e("../../misago-markup"),f=n(d),p=e("../../../utils/escape-html"),m=n(p),h='<a href="%(url)s" class="item-title">%(user)s</a>',b='<span class="item-title">%(user)s</span>',v='<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'},{"../../../utils/escape-html":379,"../../misago-markup":58,"../waypoint":170,react:"react"}],152:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){P["default"].dispatch(w.patch(e.post,{is_unapproved:!1}));var t=[{op:"replace",path:"is-unapproved",value:!1}],a={is_unapproved:e.post.is_unapproved};f(e,t,a)}function l(e){P["default"].dispatch(w.patch(e.post,{is_protected:!0}));var t=[{op:"replace",path:"is-protected",value:!0}],a={is_protected:e.post.is_protected};f(e,t,a)}function s(e){P["default"].dispatch(w.patch(e.post,{is_protected:!1}));var t=[{op:"replace",path:"is-protected",value:!1}],a={is_protected:e.post.is_protected};f(e,t,a)}function i(e){P["default"].dispatch(w.patch(e.post,{is_hidden:!0,hidden_on:(0,_["default"])(),hidden_by_name:e.user.username,url:Object.assign(e.post.url,{hidden_by:e.user.url})}));var t=[{op:"replace",path:"is-hidden",value:!0}],a={is_hidden:e.post.is_hidden,hidden_on:e.post.hidden_on,hidden_by_name:e.post.hidden_by_name,url:e.post.url};f(e,t,a)}function u(e){P["default"].dispatch(w.patch(e.post,{is_hidden:!1}));var t=[{op:"replace",path:"is-hidden",value:!1}],a={is_hidden:e.post.is_hidden};f(e,t,a)}function c(e){var t=e.post.last_likes||[],a=[e.user].concat(t),n=a.length>3?a.slice(0,-1):a;P["default"].dispatch(w.patch(e.post,{is_liked:!0,likes:e.post.likes+1,last_likes:n}));var r=[{op:"replace",path:"is-liked",value:!0}],o={is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes};f(e,r,o)}function d(e){P["default"].dispatch(w.patch(e.post,{is_liked:!1,likes:e.post.likes-1,last_likes:e.post.last_likes.filter(function(t){return!t.id||t.id!==e.user.id})}));var t=[{op:"replace",path:"is-liked",value:!1}],a={is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes};f(e,t,a)}function f(e,t,a){k["default"].patch(e.post.api.index,t).then(function(t){P["default"].dispatch(w.patch(e.post,t))},function(t){400===t.status?x["default"].error(t.detail[0]):x["default"].apiError(t),P["default"].dispatch(w.patch(e.post,a))})}function p(e){var t=confirm(gettext("Are you sure you want to delete this post? This action is not reversible!"));t&&(P["default"].dispatch(w.patch(e.post,{isDeleted:!0})),k["default"]["delete"](e.post.api.index).then(function(){x["default"].success(gettext("Post has been deleted."))},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),P["default"].dispatch(w.patch(e.post,{isDeleted:!1}))}))}function m(e){var t=e.post,a=e.user;P["default"].dispatch(y.update({best_answer:t.id,best_answer_is_protected:t.is_protected,best_answer_marked_on:(0,_["default"])(),best_answer_marked_by:a.id,best_answer_marked_by_name:a.username,best_answer_marked_by_slug:a.slug}));var n=[{op:"replace",path:"best-answer",value:t.id},{op:"add",path:"acl",value:!0}],r={best_answer:e.thread.best_answer,best_answer_is_protected:e.thread.best_answer_is_protected,best_answer_marked_on:e.thread.best_answer_marked_on,best_answer_marked_by:e.thread.best_answer_marked_by,best_answer_marked_by_name:e.thread.best_answer_marked_by_name,best_answer_marked_by_slug:e.thread.best_answer_marked_by_slug};b(e,n,r)}function h(e){var t=e.post;P["default"].dispatch(y.update({best_answer:null,best_answer_is_protected:!1,best_answer_marked_on:null,best_answer_marked_by:null,best_answer_marked_by_name:null,best_answer_marked_by_slug:null}));var a=[{op:"remove",path:"best-answer",value:t.id},{op:"add",path:"acl",value:!0}],n={best_answer:e.thread.best_answer,best_answer_is_protected:e.thread.best_answer_is_protected,best_answer_marked_on:e.thread.best_answer_marked_on,best_answer_marked_by:e.thread.best_answer_marked_by,best_answer_marked_by_name:e.thread.best_answer_marked_by_name,best_answer_marked_by_slug:e.thread.best_answer_marked_by_slug};b(e,a,n)}function b(e,t,a){k["default"].patch(e.thread.api.index,t).then(function(e){e.best_answer_marked_on&&(e.best_answer_marked_on=(0,_["default"])(e.best_answer_marked_on)),P["default"].dispatch(y.update(e))},function(e){400===e.status?x["default"].error(e.detail[0]):x["default"].apiError(e),P["default"].dispatch(y.update(a))})}Object.defineProperty(a,"__esModule",{value:!0}),a.approve=o,a.protect=l,a.unprotect=s,a.hide=i,a.unhide=u,a.like=c,a.unlike=d,a.patch=f,a.remove=p,a.markAsBestAnswer=m,a.unmarkBestAnswer=h,a.patchThread=b;var v=e("moment"),_=r(v),g=e("../../../../reducers/thread"),y=n(g),E=e("../../../../reducers/post"),w=n(E),O=e("../../../../services/ajax"),k=r(O),N=e("../../../../services/snackbar"),x=r(N),j=e("../../../../services/store"),P=r(j)},{"../../../../reducers/post":349,"../../../../reducers/thread":356,"../../../../services/ajax":361,"../../../../services/snackbar":372,"../../../../services/store":373,moment:"moment"}],153:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=a.Unprotect=a.Protect=a.Split=a.Move=a.Approve=a.PostEdits=a.UnmarkMarkBestAnswer=a.MarkAsBestAnswer=a.Edit=a.Permalink=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return c["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},c["default"].createElement(O,e),c["default"].createElement(k,e),c["default"].createElement(N,e),c["default"].createElement(x,e),c["default"].createElement(j,e),c["default"].createElement(P,e),c["default"].createElement(C,e),c["default"].createElement(M,e),c["default"].createElement(S,e),c["default"].createElement(T,e),c["default"].createElement(L,e),c["default"].createElement(A,e),c["default"].createElement(R,e))};var u=e("react"),c=r(u),d=e("../../../../services/modal"),f=r(d),p=e("../../../../services/posting"),m=r(p),h=e("./actions"),b=n(h),v=e("./move"),_=r(v),g=e("../../../post-changelog"),y=r(g),E=e("./split"),w=r(E),O=a.Permalink=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=window.location.protocol+"//";e+=window.location.host,e+=n.props.post.url.index,prompt(gettext("Permament link to this post:"),e)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"link"),gettext("Permament link")))}}]),t}(c["default"].Component),k=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m["default"].open({mode:"EDIT",config:n.props.post.api.editor,submit:n.props.post.api.index})},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_edit?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"edit"),gettext("Edit"))):null}}]),t}(c["default"].Component),N=a.MarkAsBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?t.id===a.best_answer?null:a.best_answer&&!a.acl.can_change_best_answer?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Mark as best answer"))):null}}]),t}(c["default"].Component),x=a.UnmarkMarkBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unmarkBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id!==a.best_answer?null:a.acl.can_unmark_best_answer?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"check_box_outline_blank"),gettext("Unmark best answer"))):null}}]),t}(c["default"].Component),j=a.PostEdits=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(y["default"],{post:n.props.post}))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("This post was edited %(edits)s time.","This post was edited %(edits)s times.",this.props.post.edits);interpolate(a,{edits:this.props.post.edits},!0);return c["default"].createElement("li",null,c["default"].createElement("button",{
-className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"edit"),gettext("Changes history")))}}]),t}(c["default"].Component),P=a.Approve=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.approve(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_approve&&this.props.post.is_unapproved?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}}]),t}(c["default"].Component),C=a.Move=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(_["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}}]),t}(c["default"].Component),M=a.Split=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(w["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_split"),gettext("Split"))):null}}]),t}(c["default"].Component),S=a.Protect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.protect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_protect?this.props.post.is_protected?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Protect"))):null}}]),t}(c["default"].Component),T=a.Unprotect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unprotect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_protect&&this.props.post.is_protected?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Remove protection"))):null}}]),t}(c["default"].Component),L=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.hide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id===a.best_answer?null:t.acl.can_hide?t.is_hidden?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}}]),t}(c["default"].Component),A=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unhide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_unhide&&this.props.post.is_hidden?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}}]),t}(c["default"].Component),R=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.remove(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id===a.best_answer?null:t.acl.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}}]),t}(c["default"].Component)},{"../../../../services/modal":367,"../../../../services/posting":371,"../../../post-changelog":116,"./actions":152,"./move":155,"./split":156,react:"react"}],154:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"pull-right dropdown"},o["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default btn-icon dropdown-toggle","data-toggle":"dropdown",type:"button"},o["default"].createElement("span",{className:"material-icon"},"expand_more")),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./dropdown"),s=n(l)},{"./dropdown":153,react:"react"}],155:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Move post")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../button"),p=(r(f),e("../../../form")),m=r(p),h=e("../../../form-group"),b=r(h),v=e("../../../../reducers/post"),_=n(v),g=e("../../../../services/ajax"),y=r(g),E=e("../../../../services/modal"),w=r(E),O=e("../../../../services/snackbar"),k=r(O),N=e("../../../../services/store"),x=r(N),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(k["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return y["default"].post(this.props.thread.api.posts.move,{new_thread:this.state.url,posts:[this.props.post.id]})}},{key:"handleSuccess",value:function(e){x["default"].dispatch(_.patch(this.props.post,{isDeleted:!0})),w["default"].hide(),k["default"].success(gettext("Selected post was moved to the other thread."))}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail):k["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(b["default"],{"for":"id_url",label:gettext("Link to thread you want to move post to")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading},gettext("Move post"))))))}}]),t}(m["default"]);a["default"]=j},{"../../../../reducers/post":349,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../button":7,"../../../form":54,"../../../form-group":53,react:"react"}],156:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement(k["default"],null))}function u(e){return m["default"].createElement(c,{className:"modal-dialog modal-message"},m["default"].createElement("div",{className:"message-icon"},m["default"].createElement("span",{className:"material-icon"},"info_outline")),m["default"].createElement("div",{className:"message-body"},m["default"].createElement("p",{className:"lead"},gettext("You can't move this post at the moment.")),m["default"].createElement("p",null,e.message)))}function c(e){return m["default"].createElement("div",{className:e.className,role:"document"},m["default"].createElement("div",{className:"modal-content"},m["default"].createElement("div",{className:"modal-header"},m["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},m["default"].createElement("span",{"aria-hidden":"true"},"×")),m["default"].createElement("h4",{className:"modal-title"},gettext("Split post into new thread"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0}),a.ModerationForm=a.PostingConfig=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return m["default"].createElement(B,f({},e,{Form:H}))},a.Loader=i,a.Error=u,a.Modal=c;var p=e("react"),m=r(p),h=e("../../../button"),b=r(h),v=e("../../../form"),_=r(v),g=e("../../../form-group"),y=r(g),E=e("../../../category-select"),w=r(E),O=e("../../../modal-loader"),k=r(O),N=e("../../../select"),x=r(N),j=e("../../../../reducers/post"),P=n(j),C=e("../../../../services/ajax"),M=r(C),S=e("../../../../services/modal"),T=r(S),L=e("../../../../services/snackbar"),A=r(L),R=e("../../../../services/store"),I=r(R),D=e("../../../../utils/validators"),U=n(D),B=a.PostingConfig=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isError:!1,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;M["default"].get(misago.get("THREAD_EDITOR_API")).then(function(t){var a=t.map(function(e){return Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id,post:e.post})});e.setState({isLoaded:!0,categories:a})},function(t){e.setState({isError:t.detail})})}},{key:"render",value:function(){return this.state.isError?m["default"].createElement(u,{message:this.state.isError}):this.state.isLoaded?m["default"].createElement(H,f({},this.props,{categories:this.state.categories})):m["default"].createElement(i,null)}}]),t}(m["default"].Component),H=a.ModerationForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,categories:e.categories,weight:0,is_hidden:0,is_closed:!1,validators:{title:[U.required()]},errors:{}},a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a.acl={},a.props.categories.forEach(function(e){e.post&&(a.state.category||(a.state.category=e.id),a.acl[e.id]={can_pin_threads:e.post.pin,can_close_threads:e.post.close,can_hide_threads:e.post.hide})}),a}return s(t,e),d(t,[{key:"clean",value:function(){return!!this.isValid()||(A["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return M["default"].post(this.props.thread.api.posts.split,{title:this.state.title,category:this.state.category,weight:this.state.weight,is_hidden:this.state.is_hidden,is_closed:this.state.is_closed,posts:[this.props.post.id]})}},{key:"handleSuccess",value:function(e){I["default"].dispatch(P.patch(this.props.post,{isDeleted:!0})),T["default"].hide(),A["default"].success(gettext("Selected post was split into new thread."))}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),A["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?T["default"].show(m["default"].createElement(ErrorsModal,{errors:e})):A["default"].apiError(e)}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?m["default"].createElement(y["default"],{label:gettext("Thread weight"),"for":"id_weight",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?m["default"].createElement(y["default"],{label:gettext("Hide thread"),"for":"id_is_hidden",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?m["default"].createElement(y["default"],{label:gettext("Close thread"),"for":"id_is_closed",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"render",value:function(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement("form",{onSubmit:this.handleSubmit},m["default"].createElement("div",{className:"modal-body"},m["default"].createElement(y["default"],{label:gettext("Thread title"),"for":"id_title",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.title},m["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),m["default"].createElement("div",{className:"clearfix"}),m["default"].createElement(y["default"],{label:gettext("Category"),"for":"id_category",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.category},m["default"].createElement(w["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.state.categories})),m["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),m["default"].createElement("div",{className:"modal-footer"},m["default"].createElement(b["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Split post")))))}}]),t}(_["default"])},{"../../../../reducers/post":349,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../../utils/validators":389,"../../../button":7,"../../../category-select":20,"../../../form":54,"../../../form-group":53,"../../../modal-loader":59,"../../../select":207,react:"react"}],157:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.post,a=e.thread,n=e.user;if(!i(t)||t.id!==a.best_answer)return null;var r=null;return r=n.id&&a.best_answer_marked_by===n.id?interpolate(gettext("Marked as best answer by you %(marked_on)s."),{marked_on:a.best_answer_marked_on.fromNow()},!0):interpolate(gettext("Marked as best answer by %(marked_by)s %(marked_on)s."),{marked_by:a.best_answer_marked_by_name,marked_on:a.best_answer_marked_on.fromNow()},!0),c["default"].createElement("div",{className:"post-status-message post-status-best-answer"},c["default"].createElement("span",{className:"material-icon"},"check_box"),c["default"].createElement("p",null,r))}function o(e){return i(e.post)&&e.post.is_hidden?c["default"].createElement("div",{className:"post-status-message post-status-hidden"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),c["default"].createElement("p",null,gettext("This post is hidden. Only users with permission may see its contents."))):null}function l(e){return i(e.post)&&e.post.is_unapproved?c["default"].createElement("div",{className:"post-status-message post-status-unapproved"},c["default"].createElement("span",{className:"material-icon"},"remove_circle_outline"),c["default"].createElement("p",null,gettext("This post is unapproved. Only users with permission to approve posts and its author may see its contents."))):null}function s(e){return i(e.post)&&e.post.is_protected?c["default"].createElement("div",{className:"post-status-message post-status-protected visible-xs-block"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),c["default"].createElement("p",null,gettext("This post is protected. Only moderators may change it."))):null}function i(e){return!e.is_hidden||e.acl.can_see_hidden}Object.defineProperty(a,"__esModule",{value:!0}),a.FlagBestAnswer=r,a.FlagHidden=o,a.FlagUnapproved=l,a.FlagProtected=s,a.isVisible=i;var u=e("react"),c=n(u)},{react:"react"}],158:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return(!e.is_hidden||e.acl.can_see_hidden)&&(e.acl.can_reply||e.acl.can_edit||e.acl.can_see_likes&&(e.last_likes||[]).length||e.acl.can_like)}function u(e,t){var a=t.slice(0,3).map(function(e){return e.username});if(1==a.length)return interpolate(gettext("%(user)s likes this."),{user:a[0]},!0);var n=e-a.length,r=a.slice(0,-1).join(", "),o=a.slice(-1)[0],l=interpolate(gettext("%(users)s and %(last_user)s"),{users:r,last_user:o},!0);if(0===n)return interpolate(gettext("%(users)s like this."),{users:l},!0);var s=ngettext("%(users)s and %(likes)s other user like this.","%(users)s and %(likes)s other users like this.",n);return interpolate(s,{users:a.join(", "),likes:n},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a.Edit=a.Reply=a.LikesCompact=a.Likes=a.Like=a.MarkAsBestAnswerCompact=a.MarkAsBestAnswer=void 0;var c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),d=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return i(e.post)?p["default"].createElement("div",{className:"post-footer"},p["default"].createElement(w,e),p["default"].createElement(O,e),p["default"].createElement(k,e),p["default"].createElement(N,d({lastLikes:e.post.last_likes,likes:e.post.likes},e)),p["default"].createElement(x,d({likes:e.post.likes},e)),p["default"].createElement(j,e),p["default"].createElement(P,e)):null},a.isVisible=i,a.getLikesMessage=u;var f=e("react"),p=r(f),m=e("./controls/actions"),h=n(m),b=e("../../post-likes"),v=r(b),_=e("../../../services/modal"),g=r(_),y=e("../../../services/posting"),E=r(y),w=a.MarkAsBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){h.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?a.best_answer&&!a.acl.can_change_best_answer?null:p["default"].createElement("button",{className:"hidden-xs btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||t.id===a.best_answer,onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Best answer")):null}}]),t}(p["default"].Component),O=a.MarkAsBestAnswerCompact=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){h.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?a.best_answer&&!a.acl.can_change_best_answer?null:p["default"].createElement("button",{className:"visible-xs-inline-block btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||t.id===a.best_answer,onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"check_box")):null}}]),t}(p["default"].Component),k=a.Like=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.post.is_liked?h.unlike(n.props):h.like(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){if(!this.props.post.acl.can_like)return null;var e="btn btn-default btn-sm pull-left";return this.props.post.is_liked&&(e="btn btn-success btn-sm pull-left"),p["default"].createElement("button",{className:e,disabled:this.props.post.isBusy,onClick:this.onClick,type:"button"},this.props.post.is_liked?gettext("Liked"):gettext("Like"))}}]),t}(p["default"].Component),N=a.Likes=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){g["default"].show(p["default"].createElement(v["default"],{post:n.props.post}))},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?p["default"].createElement("button",{className:"btn btn-link btn-sm pull-left hidden-xs",onClick:this.onClick,type:"button"},u(this.props.likes,this.props.lastLikes)):p["default"].createElement("p",{className:"pull-left hidden-xs"},u(this.props.likes,this.props.lastLikes)):null}}]),t}(p["default"].Component),x=a.LikesCompact=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),c(t,[{key:"render",value:function(){var e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?p["default"].createElement("button",{className:"btn btn-link btn-sm likes-compact pull-left visible-xs-block",onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"favorite"),this.props.likes):p["default"].createElement("p",{className:"likes-compact pull-left visible-xs-block"},p["default"].createElement("span",{className:"material-icon"},"favorite"),this.props.likes):null}}]),t}(N),j=a.Reply=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].open({mode:"REPLY",config:n.props.thread.api.editor,submit:n.props.thread.api.posts.index,context:{reply:n.props.post.id}})},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){return this.props.post.acl.can_reply?p["default"].createElement("button",{className:"btn btn-primary btn-sm pull-right",type:"button",onClick:this.onClick},gettext("Reply")):null}}]),t}(p["default"].Component),P=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].open({mode:"EDIT",config:n.props.post.api.editor,submit:n.props.post.api.index})},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){return this.props.post.acl.can_edit?p["default"].createElement("button",{className:"hidden-xs btn btn-default btn-sm pull-right",type:"button",onClick:this.onClick},gettext("Edit")):null}}]),t}(p["default"].Component)},{"../../../services/modal":367,"../../../services/posting":371,"../../post-likes":129,"./controls/actions":152,react:"react"}],159:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.post.is_read?null:m["default"].createElement("span",{className:"label label-unread hidden-xs"},gettext("New post"))}function i(e){return e.post.is_read?null:m["default"].createElement("span",{className:"label label-unread visible-xs-inline-block"},gettext("New"))}function u(e){var t=interpolate(gettext("posted %(posted_on)s"),{posted_on:e.post.posted_on.format("LL, LT")},!0);return m["default"].createElement("a",{href:e.post.url.index,className:"btn btn-link posted-on hidden-xs",title:t},e.post.posted_on.fromNow())}function c(e){return m["default"].createElement("a",{href:e.post.url.index,className:"btn btn-link posted-on visible-xs-inline-block"},e.post.posted_on.fromNow(!0))}function d(e){var t=e.post.poster&&e.post.poster.id===e.user.id,a=e.post.acl.can_protect,n=e.user.id&&e.post.is_protected&&(t||a);return n?m["default"].createElement("span",{className:"label label-protected hidden-xs",title:gettext("This post is protected and may not be edited.")},m["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("protected")):null}Object.defineProperty(a,"__esModule",{value:!0}),a.PostEditsCompacts=a.PostEdits=void 0;var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return m["default"].createElement("div",{className:"post-heading"},m["default"].createElement(s,e),m["default"].createElement(i,e),m["default"].createElement(u,e),m["default"].createElement(c,e),m["default"].createElement(O,e),m["default"].createElement(k,e),m["default"].createElement(d,e),m["default"].createElement(_["default"],e),m["default"].createElement(b["default"],e))},a.UnreadLabel=s,a.UnreadCompact=i,a.PostedOn=u,a.PostedOnCompact=c,a.ProtectedLabel=d;var p=e("react"),m=n(p),h=e("./controls"),b=n(h),v=e("./select"),_=n(v),g=(e("../../user-status"),e("../../post-changelog")),y=n(g),E=e("../../../services/modal"),w=n(E),O=a.PostEdits=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){w["default"].show(m["default"].createElement(y["default"],{post:n.props.post}))},l=a,o(n,l)}return l(t,e),f(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("This post was edited %(edits)s time.","This post was edited %(edits)s times.",this.props.post.edits),n=interpolate(a,{
-edits:this.props.post.edits},!0),r=ngettext("edited %(edits)s time","edited %(edits)s times",this.props.post.edits);return m["default"].createElement("button",{className:"btn btn-link btn-see-edits hidden-xs",onClick:this.onClick,title:n,type:"button"},interpolate(r,{edits:this.props.post.edits},!0))}}]),t}(m["default"].Component),k=a.PostEditsCompacts=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),f(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("%(edits)s edit","%(edits)s edits",this.props.post.edits);return m["default"].createElement("button",{className:"btn btn-link btn-see-edits visible-xs-inline-block",onClick:this.onClick,type:"button"},interpolate(a,{edits:this.props.post.edits},!0))}}]),t}(O)},{"../../../services/modal":367,"../../post-changelog":116,"../../user-status":271,"./controls":154,"./select":169,react:"react"}],160:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t="post";return e.post.isDeleted?t="hide":e.post.is_hidden&&!e.post.acl.can_see_hidden&&(t="post post-hidden"),e.post.poster&&e.post.poster.rank.css_class&&(t+=" post-"+e.post.poster.rank.css_class),e.post.is_read||(t+=" post-new"),o["default"].createElement("li",{id:"post-"+e.post.id,className:t},o["default"].createElement("div",{className:"panel panel-default panel-post"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"row"},o["default"].createElement(b["default"],e),o["default"].createElement("div",{className:"col-xs-12 col-md-9"},o["default"].createElement(m["default"],e),o["default"].createElement(c.FlagBestAnswer,e),o["default"].createElement(c.FlagUnapproved,e),o["default"].createElement(c.FlagProtected,e),o["default"].createElement(c.FlagHidden,e),o["default"].createElement(u["default"],e),o["default"].createElement(s["default"],e),o["default"].createElement(f["default"],e))))))};var r=e("react"),o=n(r),l=e("./attachments"),s=n(l),i=e("./body"),u=n(i),c=e("./flags"),d=e("./footer"),f=n(d),p=e("./header"),m=n(p),h=e("./post-side"),b=n(h)},{"./attachments":150,"./body":151,"./flags":157,"./footer":158,"./header":159,"./post-side":163,react:"react"}],161:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.thread;return o["default"].createElement("div",{className:"col-xs-12 col-md-3 post-side post-side-anonymous"},o["default"].createElement(d["default"],{post:t,thread:a}),o["default"].createElement(u["default"],{post:t,thread:a}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("span",null,o["default"].createElement(s["default"],{className:"poster-avatar",size:100}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("span",{className:"media-heading item-title"},t.poster_name),o["default"].createElement("span",{className:"user-title user-title-anonymous"},gettext("Removed user")))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("../controls"),u=n(i),c=e("../select"),d=n(c),f=e("../../../user-status"),p=(n(f),e("./user-postcount")),m=(n(p),e("./user-title"));n(m)},{"../../../avatar":5,"../../../user-status":271,"../controls":154,"../select":169,"./user-postcount":165,"./user-title":167,react:"react"}],162:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.title,a=e.rank;return a.is_tab||!!t||!!a.title}},{}],163:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.poster?o["default"].createElement(u["default"],e):o["default"].createElement(s["default"],e)};var r=e("react"),o=n(r),l=e("./anonymous"),s=n(l),i=e("./registered"),u=n(i)},{"./anonymous":161,"./registered":164,react:"react"}],164:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.thread,n=t.poster;return o["default"].createElement("div",{className:"col-xs-12 col-md-3 post-side post-side-registered"},o["default"].createElement(d["default"],{post:t,thread:a}),o["default"].createElement(u["default"],{post:t,thread:a}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("a",{href:n.url},o["default"].createElement(s["default"],{className:"poster-avatar",size:100,user:n}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("a",{className:"item-title",href:n.url},n.username),o["default"].createElement(p["default"],{status:n.status},o["default"].createElement(f.StatusIcon,{status:n.status}))),o["default"].createElement(g["default"],{rank:n.rank,title:n.title}),o["default"].createElement(v["default"],{poster:n}),o["default"].createElement(h["default"],{poster:n}))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("../controls"),u=n(i),c=e("../select"),d=n(c),f=e("../../../user-status"),p=n(f),m=e("./user-postcount"),h=n(m),b=e("./user-status"),v=n(b),_=e("./user-title"),g=n(_)},{"../../../avatar":5,"../../../user-status":271,"../controls":154,"../select":169,"./user-postcount":165,"./user-status":166,"./user-title":167,react:"react"}],165:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.poster,a=ngettext("%(posts)s post","%(posts)s posts",t.posts),n="user-postcount";return(0,s["default"])(t)&&(n+=" hidden-xs hidden-sm"),o["default"].createElement("span",{className:n},interpolate(a,{posts:t.posts},!0))};var r=e("react"),o=n(r),l=e("./has-visible-title"),s=n(l)},{"./has-visible-title":162,react:"react"}],166:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.poster,a="hidden-xs";return(0,u["default"])(t)&&(a+=" hidden-sm"),o["default"].createElement("span",{className:a},o["default"].createElement(s["default"],{status:t.status},o["default"].createElement(l.StatusLabel,{status:t.status,user:t})))};var r=e("react"),o=n(r),l=e("../../../user-status"),s=n(l),i=e("./has-visible-title"),u=n(i)},{"../../../user-status":271,"./has-visible-title":162,react:"react"}],167:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.rank,a=e.title,n=a||t.title;if(!n&&t.is_tab&&(n=t.name),!n)return null;var r="user-title";return t.css_class&&(r+=" user-title-"+t.css_class),t.is_tab?o["default"].createElement("div",{className:r},o["default"].createElement("a",{href:t.url},n)):o["default"].createElement("div",{className:r},n)};var r=e("react"),o=n(r)},{react:"react"}],168:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("li",{className:"post"},l["default"].createElement("div",{className:"post-border"},l["default"].createElement("div",{className:"post-avatar"},l["default"].createElement(i["default"],{size:"100"})),l["default"].createElement("div",{className:"post-body"},l["default"].createElement("div",{className:"panel panel-default panel-post"},l["default"].createElement("div",{className:"panel-heading post-heading"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,100)+"px"}}," "),l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,100)+"px"}}," ")),l["default"].createElement("div",{className:"panel-body"},l["default"].createElement("article",{className:"misago-markup"},l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," "),l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," "),l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," ")))))))};var o=e("react"),l=r(o),s=e("../../avatar"),i=r(s),u=e("../../../utils/random"),c=n(u)},{"../../../utils/random":384,"../../avatar":5,react:"react"}],169:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.can_approve||e.can_hide||e.can_protect||e.can_unhide||e.can_delete||e.can_move}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.isVisible=i;var c=e("react"),d=r(c),f=e("../../../reducers/posts"),p=n(f),m=e("../../../services/store"),h=r(m),b=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.post.isSelected?h["default"].dispatch(p.deselect(n.props.post)):h["default"].dispatch(p.select(n.props.post))},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.thread.acl.can_merge_posts||i(this.props.post.acl)?d["default"].createElement("div",{className:"pull-right hidden-xs"},d["default"].createElement("button",{className:"btn btn-default btn-icon",onClick:this.onClick,type:"button"},d["default"].createElement("span",{className:"material-icon"},this.props.post.isSelected?"check_box":"check_box_outline_blank"))):null}}]),t}(d["default"].Component);a["default"]=b},{"../../../reducers/posts":350,"../../../services/store":373,react:"react"}],170:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../reducers/post"),f=n(d),p=e("../../reducers/thread"),m=n(p),h=e("../../services/ajax"),b=r(h),v=e("../../services/snackbar"),_=r(v),g=e("../../services/store"),y=r(g),E=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;this.props.post.is_read||$(this.documentNode).waypoint({handler:function(t){"down"!==t||e.props.post.is_read||window.setTimeout(function(){var t=e.documentNode.getBoundingClientRect(),a=t.height+t.top,n=document.documentElement.clientHeight;a<5||a>n||(y["default"].dispatch(f.patch(e.props.post,{is_read:!0})),b["default"].post(e.props.post.api.read).then(function(t){y["default"].dispatch(m.update(e.props.thread,{is_read:t.thread_is_read}))},function(e){_["default"].apiError(e)}))},1e3)},offset:"bottom-in-view"})}},{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:this.props.className,ref:function(t){e.documentNode=t}},this.props.children)}}]),t}(c["default"].Component);a["default"]=E},{"../../reducers/post":349,"../../reducers/thread":356,"../../services/ajax":361,"../../services/snackbar":372,"../../services/store":373,react:"react"}],171:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=e("../panel-loader"),p=n(f),m=e("../panel-message"),h=n(m),b=e("../../index"),v=n(b),_=e("../../services/polls"),g=n(_),y=e("../../services/page-title"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){e.expires_on&&(e.expires_on=(0,u["default"])(e.expires_on)),a.setState({isLoaded:!0,error:null,ban:e})},a.error=function(e){a.setState({isLoaded:!0,error:e.detail,ban:null})},v["default"].has("PROFILE_BAN")?a.initWithPreloadedData(v["default"].pop("PROFILE_BAN")):a.initWithoutPreloadedData(),a.startPolling(e.profile.api.ban),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){e.expires_on&&(e.expires_on=(0,u["default"])(e.expires_on)),this.state={isLoaded:!0,ban:e}}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(e){g["default"].start({poll:"ban-details",url:e,frequency:9e4,update:this.update,error:this.error})}},{key:"componentDidMount",value:function(){E["default"].set({title:gettext("Ban details"),parent:this.props.profile.username})}},{key:"componentWillUnmount",value:function(){g["default"].stop("ban-details")}},{key:"getUserMessage",value:function(){return this.state.ban.user_message?d["default"].createElement("div",{className:"panel-body ban-message ban-user-message"},d["default"].createElement("h4",null,gettext("User-shown ban message")),d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.user_message.html}})):null}},{key:"getStaffMessage",value:function(){return this.state.ban.staff_message?d["default"].createElement("div",{className:"panel-body ban-message ban-staff-message"},d["default"].createElement("h4",null,gettext("Team-shown ban message")),d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.staff_message.html}})):null}},{key:"getExpirationMessage",value:function(){if(this.state.ban.expires_on){if(this.state.ban.expires_on.isAfter((0,u["default"])())){var e=interpolate(gettext("This ban expires on %(expires_on)s."),{expires_on:this.state.ban.expires_on.format("LL, LT")},!0),t=interpolate(gettext("This ban expires %(expires_on)s."),{expires_on:this.state.ban.expires_on.fromNow()},!0);return d["default"].createElement("abbr",{title:e},t)}return gettext("This ban has expired.")}return interpolate(gettext("%(username)s's ban is permanent."),{username:this.props.profile.username},!0)}},{key:"getPanelBody",value:function(){return this.state.ban?Object.keys(this.state.ban).length?d["default"].createElement("div",null,this.getUserMessage(),this.getStaffMessage(),d["default"].createElement("div",{className:"panel-body ban-expires"},d["default"].createElement("h4",null,gettext("Ban expiration")),d["default"].createElement("p",{className:"lead"},this.getExpirationMessage()))):d["default"].createElement("div",null,d["default"].createElement(h["default"],{message:gettext("No ban is active at the moment.")})):this.state.error?d["default"].createElement("div",null,d["default"].createElement(h["default"],{icon:"error_outline",message:this.state.error})):d["default"].createElement("div",null,d["default"].createElement(p["default"],null))}},{key:"render",value:function(){return d["default"].createElement("div",{className:"profile-ban-details"},d["default"].createElement("div",{className:"panel panel-default"},d["default"].createElement("div",{className:"panel-heading"},d["default"].createElement("h3",{className:"panel-title"},gettext("Ban details"))),this.getPanelBody()))}}]),t}(d["default"].Component);a["default"]=w},{"../../index":299,"../../services/page-title":369,"../../services/polls":370,"../panel-loader":90,"../panel-message":91,moment:"moment",react:"react"}],172:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.isAuthenticated,a=e.profile,n=null;return n=t?gettext("You are not sharing any details with others."):interpolate(gettext("%(username)s is not sharing any details with others."),{username:a.username},!0),o["default"].createElement("div",{className:"panel panel-default"},o["default"].createElement("div",{className:"panel-body text-center lead"},n))};var r=e("react"),o=n(r)},{react:"react"}],173:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.text,a=e.url;return a?l["default"].createElement("p",null,l["default"].createElement("a",{href:a,target:"_blank",rel:"nofollow"},t||a)):t?l["default"].createElement("p",null,t):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.html,a=e.text,n=e.url;return t?l["default"].createElement("div",{className:"form-control-static col-md-9",dangerouslySetInnerHTML:{__html:t}}):l["default"].createElement("div",{className:"form-control-static col-md-9"},l["default"].createElement(r,{text:a,url:n}))},a.SafeValue=r;var o=e("react"),l=n(o)},{react:"react"}],174:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"form-group"},o["default"].createElement("strong",{className:"control-label col-md-3"},e.name,":"),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./field-value"),s=n(l)},{"./field-value":173,react:"react"}],175:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.api,a=e.display,n=e.onCancel,r=e.onSuccess;return a?o["default"].createElement(s["default"],{api:t,onCancel:n,onSuccess:r}):null};var r=e("react"),o=n(r),l=e("../../edit-details"),s=n(l)},{"../../edit-details":31,react:"react"}],176:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.fields,a=e.name;return o["default"].createElement("div",{className:"panel panel-default panel-profile-details-group"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},a)),o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"form-horizontal"},t.map(function(e){var t=e.fieldname,a=e.html,n=e.name,r=e.text,l=e.url;return o["default"].createElement(s["default"],{key:t,name:n,html:a,text:r,url:l})}))))};var r=e("react"),o=n(r),l=e("./field"),s=n(l)},{"./field":174,react:"react"}],177:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display,a=e.groups,n=e.isAuthenticated,r=e.loading,l=e.profile;return t?r?o["default"].createElement(d["default"],null):a.length?o["default"].createElement("div",null,a.map(function(e,t){return o["default"].createElement(u["default"],{fields:e.fields,key:t,name:e.name})})):o["default"].createElement(s["default"],{isAuthenticated:n,profile:l}):null};var r=e("react"),o=n(r),l=e("./empty-message"),s=n(l),i=e("./group"),u=n(i),c=e("../../loader"),d=n(c)},{"../../loader":56,"./empty-message":172,"./group":176,react:"react"}],178:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.onEdit,a=e.showEditButton;return a?l["default"].createElement("div",{className:"col-sm-4 col-md-2"},l["default"].createElement("button",{className:"btn btn-default btn-outline btn-block",onClick:t,type:"button"},gettext("Edit"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.onEdit,a=e.showEditButton;return l["default"].createElement("div",null,l["default"].createElement("nav",{className:"toolbar"},l["default"].createElement("div",{className:"row"},l["default"].createElement("div",{className:"col-sm-8 col-md-10"},l["default"].createElement("h3",{className:"md-margin-top-no"},gettext("Details"))),l["default"].createElement(r,{onEdit:t,showEditButton:a}))))},a.EditButton=r;var o=e("react"),l=n(o)},{react:"react"}],179:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./form"),d=n(c),f=e("./groups-list"),p=n(f),m=e("./header"),h=n(m),b=e("../../../data/profile-details"),v=n(b),_=e("../../../reducers/profile-details"),g=e("../../../services/page-title"),y=n(g),E=e("../../../services/snackbar"),w=n(E),O=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCancel=function(){a.setState({editing:!1})},a.onEdit=function(){a.setState({editing:!0})},a.onSuccess=function(e){var t=a.props,n=t.dispatch,r=t.isAuthenticated,o=t.profile,l=null;l=r?gettext("Your details have been updated."):interpolate(gettext("%(username)s's details have been updated."),{username:o.username},!0),w["default"].info(l),n((0,_.load)(e)),a.setState({editing:!1})},a.state={editing:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){y["default"].set({title:gettext("Details"),parent:this.props.profile.username})}},{key:"render",value:function(){var e=this.props,t=e.dispatch,a=e.isAuthenticated,n=e.profile,r=e.profileDetails,o=r.id!==n.id;return u["default"].createElement(v["default"],{data:r,dispatch:t,user:n},u["default"].createElement("div",{className:"profile-details"},u["default"].createElement(h["default"],{onEdit:this.onEdit,showEditButton:!!r.edit&&!this.state.editing}),u["default"].createElement(p["default"],{display:!this.state.editing,groups:r.groups,isAuthenticated:a,loading:o,profile:n}),u["default"].createElement(d["default"],{api:n.api.edit_details,dispatch:t,display:this.state.editing,onCancel:this.onCancel,onSuccess:this.onSuccess})))}}]),t}(u["default"].Component);a["default"]=O},{"../../../data/profile-details":298,"../../../reducers/profile-details":351,"../../../services/page-title":369,"../../../services/snackbar":372,"./form":175,"./groups-list":177,"./header":178,react:"react"}],180:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=null;t=e.user.id===e.profile.id?gettext("You have no started threads."):interpolate(gettext("%(username)s started no threads."),{username:e.profile.username},!0);var a=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){var n=ngettext("You have started %(threads)s thread.","You have started %(threads)s threads.",e.posts.count);a=interpolate(n,{threads:e.posts.count},!0)}else{var r=ngettext("%(username)s has started %(threads)s thread.","%(username)s has started %(threads)s threads.",e.posts.count);a=interpolate(r,{username:e.profile.username,threads:e.posts.count},!0)}else a=gettext("Loading...");return i["default"].createElement(c["default"],l({api:e.profile.api.threads,emptyMessage:t,header:a,title:gettext("Threads")},e))}function o(e){var t=null;t=e.user.id===e.profile.id?gettext("You have posted no messages."):interpolate(gettext("%(username)s posted no messages."),{username:e.profile.username},!0);var a=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){var n=ngettext("You have posted %(posts)s message.","You have posted %(posts)s messages.",e.posts.count);a=interpolate(n,{posts:e.posts.count},!0)}else{var r=ngettext("%(username)s has posted %(posts)s message.","%(username)s has posted %(posts)s messages.",e.posts.count);a=interpolate(r,{username:e.profile.username,posts:e.posts.count},!0)}else a=gettext("Loading...");return i["default"].createElement(c["default"],l({api:e.profile.api.posts,emptyMessage:t,header:a,title:gettext("Posts")},e))}Object.defineProperty(a,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a.Threads=r,a.Posts=o;var s=e("react"),i=n(s),u=e("./route"),c=n(u)},{"./route":181,react:"react"}],181:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.posts.count?p["default"].createElement("div",null,p["default"].createElement(h["default"],{isReady:e.posts.isLoaded,posts:e.posts.results,poster:e.profile}),p["default"].createElement(u,{isLoading:e.isLoading,loadMore:e.loadMore,more:e.posts.more})):p["default"].createElement("p",{className:"lead"},e.emptyMessage)}function u(e){return e.more?p["default"].createElement("div",{className:"pager-more"},p["default"].createElement(v["default"],{className:"btn btn-default btn-outline",loading:e.isLoading,onClick:e.loadMore},interpolate(gettext("Show more (%(more)s)"),{more:e.more},!0))):null}Object.defineProperty(a,"__esModule",{value:!0});var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Feed=i,a.LoadMoreButton=u;var f=e("react"),p=r(f),m=e("../../post-feed"),h=r(m),b=e("../../button"),v=r(b),_=e("../../../reducers/posts"),g=n(_),y=e("../../../services/page-title"),E=r(y),w=e("../../../services/ajax"),O=r(w),k=e("../../../services/snackbar"),N=r(k),x=e("../../../services/store"),j=r(x),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isLoading:!0}),a.loadItems(a.props.posts.page+1)},a.state={isLoading:!1},a}return s(t,e),d(t,[{key:"loadItems",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;O["default"].get(this.props.api,{page:t||1}).then(function(a){1===t?j["default"].dispatch(g.load(a)):j["default"].dispatch(g.append(a)),e.setState({isLoading:!1})},function(t){e.setState({isLoading:!1}),N["default"].apiError(t)})}},{key:"componentDidMount",value:function(){E["default"].set({title:this.props.title,parent:this.props.profile.username}),this.loadItems()}},{key:"render",value:function(){return p["default"].createElement("div",{className:"profile-feed"},p["default"].createElement("nav",{className:"toolbar"},p["default"].createElement("h3",{className:"toolbar-left"},this.props.header)),p["default"].createElement(i,c({isLoading:this.state.isLoading,loadMore:this.loadMore},this.props)))}}]),t}(p["default"].Component);a["default"]=P},{"../../../reducers/posts":350,"../../../services/ajax":361,"../../../services/page-title":369,"../../../services/snackbar":372,"../../../services/store":373,"../../button":7,"../../post-feed":119,react:"react"}],182:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../../reducers/profile"),p=e("../../services/ajax"),m=n(p),h=e("../../services/snackbar"),b=n(h),v=e("../../services/store"),_=n(v),g=function(e){
-function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.action=function(){a.setState({isLoading:!0}),a.props.profile.is_followed?_["default"].dispatch((0,f.patch)({is_followed:!1,followers:a.props.profile.followers-1})):_["default"].dispatch((0,f.patch)({is_followed:!0,followers:a.props.profile.followers+1})),m["default"].post(a.props.profile.api.follow).then(function(e){a.setState({isLoading:!1}),_["default"].dispatch((0,f.patch)(e))},function(e){a.setState({isLoading:!1}),b["default"].apiError(e)})},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.profile.is_followed?this.props.className+" btn-default btn-following":this.props.className+" btn-default btn-follow"}},{key:"getIcon",value:function(){return this.props.profile.is_followed?"favorite":"favorite_border"}},{key:"getLabel",value:function(){return this.props.profile.is_followed?gettext("Following"):gettext("Follow")}},{key:"render",value:function(){return u["default"].createElement(d["default"],{className:this.getClassName(),disabled:this.state.isLoading,onClick:this.action},u["default"].createElement("span",{className:"material-icon"},this.getIcon()),this.getLabel())}}]),t}(u["default"].Component);a["default"]=g},{"../../reducers/profile":352,"../../services/ajax":361,"../../services/snackbar":372,"../../services/store":373,"../button":7,react:"react"}],183:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../quick-search"),p=n(f),m=e("../users-list"),h=n(m),b=e("../../index"),v=n(b),_=e("../../reducers/users"),g=e("../../services/ajax"),y=n(g),E=e("../../services/snackbar"),w=n(E),O=e("../../services/store"),k=n(O),N=e("../../services/page-title"),x=n(N),j=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isBusy:!0}),a.loadUsers(a.state.page+1,a.state.search)},a.search=function(e){a.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),a.loadUsers(1,e.target.value)},a.setSpecialProps(),v["default"].has(a.PRELOADED_DATA_KEY)?a.initWithPreloadedData(v["default"].pop(a.PRELOADED_DATA_KEY)):a.initWithoutPreloadedData(),a}return l(t,e),s(t,[{key:"setSpecialProps",value:function(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWERS",this.TITLE=gettext("Followers"),this.API_FILTER="followers"}},{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},k["default"].dispatch((0,_.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadUsers()}},{key:"loadUsers",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=this.props.profile.api[this.API_FILTER];y["default"].get(n,{search:a,page:t||1},"user-"+this.API_FILTER).then(function(a){1===t?k["default"].dispatch((0,_.hydrate)(a.results)):k["default"].dispatch((0,_.append)(a.results)),e.setState({isLoaded:!0,isBusy:!1,count:a.count,more:a.more,page:a.page,pages:a.pages})},function(e){w["default"].apiError(e)})}},{key:"componentDidMount",value:function(){x["default"].set({title:this.TITLE,parent:this.props.profile.username})}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("You have %(users)s follower.","You have %(users)s followers.",this.state.count);return interpolate(t,{users:this.state.count},!0)}var a=ngettext("%(username)s has %(users)s follower.","%(username)s has %(users)s followers.",this.state.count);return interpolate(a,{username:this.props.profile.username,users:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("You have no followers."):interpolate(gettext("%(username)s has no followers."),{username:this.props.profile.username},!0)}},{key:"getMoreButton",value:function(){return this.state.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(d["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},interpolate(gettext("Show more (%(more)s)"),{more:this.state.more},!0))):null}},{key:"getListBody",value:function(){return this.state.isLoaded&&0===this.state.count?u["default"].createElement("p",{className:"lead"},this.getEmptyMessage()):u["default"].createElement("div",null,u["default"].createElement(h["default"],{cols:3,isReady:this.state.isLoaded,users:this.props.users}),this.getMoreButton())}},{key:"getClassName",value:function(){return"profile-"+this.API_FILTER}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("nav",{className:"toolbar"},u["default"].createElement("h3",{className:"toolbar-left"},this.getLabel()),u["default"].createElement(p["default"],{className:"toolbar-right",value:this.state.search,onChange:this.search,placeholder:gettext("Search users...")})),this.getListBody())}}]),t}(u["default"].Component);a["default"]=j},{"../../index":299,"../../reducers/users":360,"../../services/ajax":361,"../../services/page-title":369,"../../services/snackbar":372,"../../services/store":373,"../button":7,"../quick-search":194,"../users-list":281,react:"react"}],184:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=(n(i),e("./followers")),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"setSpecialProps",value:function(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWS",this.TITLE=gettext("Follows"),this.API_FILTER="follows"}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("You are following %(users)s user.","You are following %(users)s users.",this.state.count);return interpolate(t,{users:this.state.count},!0)}var a=ngettext("%(username)s is following %(users)s user.","%(username)s is following %(users)s users.",this.state.count);return interpolate(a,{username:this.props.profile.username,users:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("You are not following any users."):interpolate(gettext("%(username)s is not following any users."),{username:this.props.profile.username},!0)}}]),t}(c["default"]);a["default"]=d},{"./followers":183,react:"react"}],185:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.isActive,a=e.isDeletingAccount;if(t!==!1&&a!==!0)return null;var n=null;return n=a?gettext("This user is deleting their account."):gettext("This user's account has been disabled by administrator."),d["default"].createElement("div",{className:"alert alert-danger"},d["default"].createElement("p",null,n))}function i(e,t){var a="";return 1==e&&(a="col-xs-12"),2==e&&(a="col-xs-6 col-sm-6"),3==e&&(2==t?a="col-xs-12 col-sm-4 xs-margin-top":a+="col-xs-6 col-sm-4"),a}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.IsDisabledMessage=s,a.getColStyle=i;var c=e("react"),d=n(c),f=e("../avatar"),p=n(f),m=e("../dropdown-toggle"),h=(n(m),e("./follow-button")),b=n(h),v=e("./message-button"),_=n(v),g=e("./moderation/nav"),y=n(g),E=e("./navs"),w=e("../user-status"),O=n(w),k=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getUserStatus",value:function(){return d["default"].createElement("li",{className:"user-status-display"},d["default"].createElement(O["default"],{user:this.props.profile,status:this.props.profile.status},d["default"].createElement(w.StatusIcon,{user:this.props.profile,status:this.props.profile.status}),d["default"].createElement(w.StatusLabel,{user:this.props.profile,status:this.props.profile.status,className:"status-label"})))}},{key:"getUserRank",value:function(){return this.props.profile.rank.is_tab?d["default"].createElement("li",{className:"user-rank"},d["default"].createElement("a",{href:this.props.profile.rank.url,className:"item-title"},this.props.profile.rank.name)):d["default"].createElement("li",{className:"user-rank"},d["default"].createElement("span",{className:"item-title"},this.props.profile.rank.name))}},{key:"getUserTitle",value:function(){return this.props.profile.title?d["default"].createElement("li",{className:"user-title"},this.props.profile.title):this.props.profile.rank.title?d["default"].createElement("li",{className:"user-title"},this.props.profile.rank.title):null}},{key:"getJoinedOn",value:function(){var e=interpolate(gettext("Joined on %(joined_on)s"),{joined_on:this.props.profile.joined_on.format("LL, LT")},!0),t=interpolate(gettext("Joined %(joined_on)s"),{joined_on:this.props.profile.joined_on.fromNow()},!0);return d["default"].createElement("li",{className:"user-joined-on"},d["default"].createElement("abbr",{title:e},t))}},{key:"getEmail",value:function(){return this.props.profile.email?d["default"].createElement("li",{className:"user-email"},d["default"].createElement("a",{href:"mailto:"+this.props.profile.email,className:"item-title"},this.props.profile.email)):null}},{key:"getFollowButton",value:function(){return this.props.profile.acl.can_follow?d["default"].createElement(b["default"],{className:"btn btn-block btn-outline",profile:this.props.profile}):null}},{key:"getModerationButton",value:function(){return this.props.profile.acl.can_moderate?d["default"].createElement("div",{className:"btn-group btn-group-justified"},d["default"].createElement("div",{className:"btn-group"},d["default"].createElement("button",{className:"btn btn-default btn-moderate btn-outline dropdown-toggle",type:"button","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},d["default"].createElement("span",{className:"material-icon"},"tonality"),gettext("Moderation")),d["default"].createElement(y["default"],{profile:this.props.profile}))):null}},{key:"render",value:function(){var e=this.props.profile.acl.can_follow,t=this.props.profile.acl.can_moderate,a=this.props.user.id===this.props.profile.id,n=!a&&this.props.user.acl.can_start_private_threads,r=0;e&&(r+=1),t&&(r+=1),n&&(r+=1);var o=r?2*r+1:0,l="page-header";return this.props.profile.rank.css_class&&(l+=" page-header-rank-"+this.props.profile.rank.css_class),d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:l},d["default"].createElement("div",{className:"container"},d["default"].createElement(s,{isActive:this.props.profile.is_active,isDeletingAccount:this.props.profile.is_deleting_account}),d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-9 col-md-offset-3"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-"+(12-o)},d["default"].createElement(p["default"],{className:"user-avatar user-avatar-sm",user:this.props.profile,size:"100",size2x:"200"}),d["default"].createElement("h1",null,this.props.profile.username)),!!r&&d["default"].createElement("div",{className:"col-sm-"+o},d["default"].createElement("div",{className:"row xs-margin-top sm-margin-top"},!!n&&d["default"].createElement("div",{className:i(r,0)},d["default"].createElement(_["default"],{className:"btn btn-default btn-block btn-outline",profile:this.props.profile,user:this.props.user})),!!e&&d["default"].createElement("div",{className:i(r,1)},this.getFollowButton()),!!t&&d["default"].createElement("div",{className:i(r,2)},this.getModerationButton()))))))),d["default"].createElement("div",{className:"header-stats"},d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-9 col-md-offset-3"},d["default"].createElement("ul",{className:"list-inline"},this.getUserStatus(),this.getUserRank(),this.getUserTitle(),this.getJoinedOn(),this.getEmail()))))),d["default"].createElement(E.CompactNav,{baseUrl:this.props.baseUrl,pages:this.props.pages,profile:this.props.profile})))}}]),t}(d["default"].Component);a["default"]=k},{"../avatar":5,"../dropdown-toggle":26,"../user-status":271,"./follow-button":182,"./message-button":186,"./moderation/nav":190,"./navs":191,react:"react"}],186:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../services/posting"),d=n(c),f=e("../.."),p=n(f),m=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){d["default"].open({mode:"START_PRIVATE",submit:p["default"].get("PRIVATE_THREADS_API"),to:[n.props.profile]})},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.user.acl.can_start_private_threads,t=this.props.user.id===this.props.profile.id;return!e||t?null:u["default"].createElement("button",{className:this.props.className,onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},"comment"),gettext("Message"))}}]),t}(u["default"].Component);a["default"]=m},{"../..":299,"../../services/posting":371,react:"react"}],187:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../modal-loader"),v=n(b),_=e("../../yes-no-switch"),g=n(_),y=e("../../modal-message"),E=n(y),w=e("../../../reducers/users"),O=e("../../../services/ajax"),k=n(O),N=e("../../../services/snackbar"),x=n(N),j=e("../../../services/store"),P=n(j),C=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isLoading:!1,error:null,is_avatar_locked:"",avatar_lock_user_message:"",avatar_lock_staff_message:""},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;k["default"].get(this.props.profile.api.moderate_avatar).then(function(t){e.setState({isLoaded:!0,is_avatar_locked:t.is_avatar_locked,avatar_lock_user_message:t.avatar_lock_user_message||"",avatar_lock_staff_message:t.avatar_lock_staff_message||""})},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"clean",value:function(){return!!this.isValid()||(x["default"].error(this.validate().username[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.profile.api.moderate_avatar,{is_avatar_locked:this.state.is_avatar_locked,avatar_lock_user_message:this.state.avatar_lock_user_message,avatar_lock_staff_message:this.state.avatar_lock_staff_message})}},{key:"handleSuccess",value:function(e){P["default"].dispatch((0,w.updateAvatar)(this.props.profile,e.avatar_hash)),x["default"].success(gettext("Avatar controls have been changed."))}},{key:"getFormBody",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(h["default"],{label:gettext("Lock avatar"),helpText:gettext("Locking user avatar will prohibit user from changing his avatar and will reset his/her avatar to default one."),"for":"id_is_avatar_locked"},u["default"].createElement(g["default"],{id:"id_is_avatar_locked",disabled:this.state.isLoading,iconOn:"lock_outline",iconOff:"lock_open",labelOn:gettext("Disallow user from changing avatar"),labelOff:gettext("Allow user to change avatar"),onChange:this.bindInput("is_avatar_locked"),value:this.state.is_avatar_locked})),u["default"].createElement(h["default"],{label:gettext("User message"),helpText:gettext("Optional message for user explaining why he/she is prohibited form changing avatar."),"for":"id_avatar_lock_user_message"},u["default"].createElement("textarea",{id:"id_avatar_lock_user_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_user_message"),value:this.state.avatar_lock_user_message})),u["default"].createElement(h["default"],{label:gettext("Staff message"),helpText:gettext("Optional message for forum team members explaining why user is prohibited form changing avatar."),"for":"id_avatar_lock_staff_message"},u["default"].createElement("textarea",{id:"id_avatar_lock_staff_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_staff_message"),value:this.state.avatar_lock_staff_message}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},gettext("Close")),u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes"))))}},{key:"getModalBody",value:function(){return this.state.error?u["default"].createElement(E["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():u["default"].createElement(v["default"],null)}},{key:"getClassName",value:function(){return this.state.error?"modal-dialog modal-message modal-avatar-controls":"modal-dialog modal-avatar-controls"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName(),role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Avatar controls"))),this.getModalBody()))}}]),t}(p["default"]);a["default"]=C},{"../../../reducers/users":360,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,"../../button":7,"../../form":54,"../../form-group":53,"../../modal-loader":59,"../../modal-message":60,"../../yes-no-switch":297,react:"react"}],188:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../modal-loader"),_=r(v),g=e("../../modal-message"),y=r(g),E=e("../../../reducers/username-history"),w=e("../../../reducers/users"),O=e("../../../services/ajax"),k=r(O),N=e("../../../services/snackbar"),x=r(N),j=e("../../../services/store"),P=r(j),C=e("../../../utils/validators"),M=n(C),S=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isLoading:!1,error:null,username:"",validators:{username:[M.usernameContent()]}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;k["default"].get(this.props.profile.api.moderate_username).then(function(){e.setState({isLoaded:!0})},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"clean",value:function(){return!!this.isValid()||(x["default"].error(this.validate().username[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.profile.api.moderate_username,{username:this.state.username})}},{key:"handleSuccess",value:function(e){this.setState({username:""}),P["default"].dispatch((0,E.addNameChange)(e,this.props.profile,this.props.user)),P["default"].dispatch((0,w.updateUsername)(this.props.profile,e.username,e.slug)),x["default"].success(gettext("Username has been changed."))}},{key:"getFormBody",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(b["default"],{label:gettext("New username"),"for":"id_username"},c["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change username"))))}},{key:"getModalBody",value:function(){return this.state.error?c["default"].createElement(y["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():c["default"].createElement(_["default"],null)}},{key:"getClassName",value:function(){return this.state.error?"modal-dialog modal-message modal-rename-user":"modal-dialog modal-rename-user"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Change username"))),this.getModalBody()))}}]),t}(m["default"]);a["default"]=S},{"../../../reducers/username-history":359,"../../../reducers/users":360,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,"../../../utils/validators":389,"../../button":7,"../../form":54,"../../form-group":53,"../../modal-loader":59,"../../modal-message":60,react:"react"}],189:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../modal-loader"),v=n(b),_=e("../../modal-message"),g=n(_),y=e("../../yes-no-switch"),E=n(y),w=e("../../../index"),O=n(w),k=e("../../../services/ajax"),N=n(k),x=e("../../../services/polls"),j=n(x),P=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.countdown=function(){window.setTimeout(function(){a.state.countdown>1?(a.setState({countdown:a.state.countdown-1}),a.countdown()):a.state.confirm||a.setState({confirm:!0})},1e3)},a.state={isLoaded:!1,isLoading:!1,isDeleted:!1,error:null,countdown:5,confirm:!1,with_content:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.profile.api["delete"]).then(function(){e.setState({isLoaded:!0}),e.countdown()},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"send",value:function(){return N["default"].post(this.props.profile.api["delete"],{with_content:this.state.with_content})}},{key:"handleSuccess",value:function(){j["default"].stop("user-profile"),this.state.with_content?this.setState({isDeleted:interpolate(gettext("%(username)s's account, threads, posts and other content has been deleted."),{username:this.props.profile.username},!0)}):this.setState({isDeleted:interpolate(gettext("%(username)s's account has been deleted and other content has been hidden."),{username:this.props.profile.username},!0)})}},{key:"getButtonLabel",value:function(){return this.state.confirm?interpolate(gettext("Delete %(username)s"),{username:this.props.profile.username},!0):interpolate(gettext("Please wait... (%(countdown)ss)"),{countdown:this.state.countdown},!0)}},{key:"getForm",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(h["default"],{label:gettext("User content"),"for":"id_with_content"},u["default"].createElement(E["default"],{id:"id_with_content",disabled:this.state.isLoading,labelOn:gettext("Delete together with user's account"),labelOff:gettext("Hide after deleting user's account"),onChange:this.bindInput("with_content"),value:this.state.with_content}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},gettext("Cancel")),u["default"].createElement(d["default"],{className:"btn-danger",loading:this.state.isLoading,disabled:!this.state.confirm},this.getButtonLabel())))}},{key:"getDeletedBody",value:function(){return u["default"].createElement("div",{className:"modal-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.state.isDeleted),u["default"].createElement("p",null,u["default"].createElement("a",{href:O["default"].get("USERS_LIST_URL")},gettext("Return to users list")))))}},{key:"getModalBody",value:function(){return this.state.error?u["default"].createElement(g["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.state.isDeleted?this.getDeletedBody():this.getForm():u["default"].createElement(v["default"],null);
-}},{key:"getClassName",value:function(){return this.state.error||this.state.isDeleted?"modal-dialog modal-message modal-delete-account":"modal-dialog modal-delete-account"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName(),role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Delete user account"))),this.getModalBody()))}}]),t}(p["default"]);a["default"]=P},{"../../../index":299,"../../../services/ajax":361,"../../../services/polls":370,"../../button":7,"../../form":54,"../../form-group":53,"../../modal-loader":59,"../../modal-message":60,"../../yes-no-switch":297,react:"react"}],190:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-redux"),d=e("./avatar-controls"),f=n(d),p=e("./change-username"),m=n(p),h=e("./delete-account"),b=n(h),v=e("../../../services/modal"),_=n(v),g=function(e){return{tick:e.tick,user:e.auth,profile:e.profile}},y=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showAvatarDialog=function(){_["default"].show((0,c.connect)(g)(f["default"]))},n.showRenameDialog=function(){_["default"].show((0,c.connect)(g)(m["default"]))},n.showDeleteDialog=function(){_["default"].show((0,c.connect)(g)(b["default"]))},l=a,o(n,l)}return l(t,e),s(t,[{key:"getAvatarButton",value:function(){return this.props.profile.acl.can_moderate_avatar?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showAvatarDialog},u["default"].createElement("span",{className:"material-icon"},"portrait"),gettext("Avatar controls"))):null}},{key:"getRenameButton",value:function(){return this.props.profile.acl.can_rename?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showRenameDialog},u["default"].createElement("span",{className:"material-icon"},"credit_card"),gettext("Change username"))):null}},{key:"getDeleteButton",value:function(){return this.props.profile.acl.can_delete?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showDeleteDialog},u["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete account"))):null}},{key:"render",value:function(){return u["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom",role:"menu"},this.getAvatarButton(),this.getRenameButton(),this.getDeleteButton())}}]),t}(u["default"].Component);a["default"]=y},{"../../../services/modal":367,"./avatar-controls":187,"./change-username":188,"./delete-account":189,react:"react","react-redux":"react-redux"}],191:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return c["default"].createElement("div",{className:"page-tabs hidden-md hidden-lg"},c["default"].createElement("div",{className:"container"},c["default"].createElement("ul",{className:"nav nav-pills",role:"menu"},e.pages.map(function(t){return c["default"].createElement(p["default"],{path:e.baseUrl+t.component+"/",key:t.component},c["default"].createElement(d.Link,{to:e.baseUrl+t.component+"/",onClick:e.hideNav},c["default"].createElement("span",{className:"material-icon"},t.icon),t.name))}))))}Object.defineProperty(a,"__esModule",{value:!0}),a.SideNav=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.CompactNav=s;var u=e("react"),c=n(u),d=e("react-router"),f=e("../li"),p=n(f),m=e("./follow-button"),h=(n(m),e("../../index"));n(h),a.SideNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"list-group nav-side"},this.props.pages.map(function(t){return c["default"].createElement(d.Link,{to:e.props.baseUrl+t.component+"/",className:"list-group-item",activeClassName:"active",key:t.component},c["default"].createElement("span",{className:"material-icon"},t.icon),t.name)}))}}]),t}(c["default"].Component)},{"../../index":299,"../li":55,"./follow-button":182,react:"react","react-router":"react-router"}],192:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{isAuthenticated:e.auth.user.id===e.profile.id,tick:e.tick.tick,user:e.auth.user,users:e.users,posts:e.posts,profile:e.profile,profileDetails:e["profile-details"],"username-history":e["username-history"]}}function i(){var e=[];return L["default"].get("PROFILE_PAGES").forEach(function(t){e.push(Object.assign({},t,{path:L["default"].get("PROFILE").url+t.component+"/",component:(0,f.connect)(s)(H[t.component])}))}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("./ban-details"),m=n(p),h=e("./details"),b=n(h),v=e("./feed"),_=e("./followers"),g=n(_),y=e("./follows"),E=n(y),w=e("./username-history"),O=n(w),k=e("./header"),N=n(k),x=e("./moderation/nav"),j=(n(x),e("./navs")),P=e("../avatar"),C=n(P),M=e("../with-dropdown"),S=n(M),T=e("../.."),L=n(T),A=e("../../reducers/profile"),R=e("../../services/polls"),I=n(R),D=e("../../services/store"),U=n(D),B=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){U["default"].dispatch((0,A.hydrate)(e))},a.startPolling(e.profile.api.index),a}return l(t,e),u(t,[{key:"startPolling",value:function(e){I["default"].start({poll:"user-profile",url:e,frequency:9e4,update:this.update})}},{key:"render",value:function(){var e=L["default"].get("PROFILE").url,t=L["default"].get("PROFILE_PAGES");return d["default"].createElement("div",{className:"page page-user-profile"},d["default"].createElement(N["default"],{baseUrl:e,pages:t,profile:this.props.profile,toggleNav:this.toggleNav,toggleModeration:this.toggleModeration,user:this.props.user}),d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm"},d["default"].createElement("div",{className:"profile-side-avatar"},d["default"].createElement(C["default"],{user:this.props.profile,size:"400"})),d["default"].createElement(j.SideNav,{baseUrl:e,pages:t,profile:this.props.profile})),d["default"].createElement("div",{className:"col-md-9"},this.props.children))))}}]),t}(S["default"]);a["default"]=B;var H={posts:v.Posts,threads:v.Threads,followers:g["default"],follows:E["default"],details:b["default"],"username-history":O["default"],"ban-details":m["default"]}},{"../..":299,"../../reducers/profile":352,"../../services/polls":370,"../../services/store":373,"../avatar":5,"../with-dropdown":296,"./ban-details":171,"./details":179,"./feed":180,"./followers":183,"./follows":184,"./header":185,"./moderation/nav":190,"./navs":191,"./username-history":193,react:"react","react-redux":"react-redux"}],193:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../quick-search"),p=n(f),m=e("../username-history/root"),h=n(m),b=e("../../index"),v=n(b),_=e("../../reducers/username-history"),g=e("../../services/ajax"),y=n(g),E=e("../../services/snackbar"),w=n(E),O=e("../../services/store"),k=n(O),N=e("../../services/page-title"),x=n(N),j=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isBusy:!0}),a.loadChanges(a.state.page+1,a.state.search)},a.search=function(e){a.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),a.loadChanges(1,e.target.value)},v["default"].has("PROFILE_NAME_HISTORY")?a.initWithPreloadedData(v["default"].pop("PROFILE_NAME_HISTORY")):a.initWithoutPreloadedData(),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},k["default"].dispatch((0,_.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadChanges()}},{key:"loadChanges",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;y["default"].get(v["default"].get("USERNAME_CHANGES_API"),{user:this.props.profile.id,search:a,page:t||1},"search-username-history").then(function(a){1===t?k["default"].dispatch((0,_.hydrate)(a.results)):k["default"].dispatch((0,_.append)(a.results)),e.setState({isLoaded:!0,isBusy:!1,count:a.count,more:a.more,page:a.page,pages:a.pages})},function(e){w["default"].apiError(e)})}},{key:"componentDidMount",value:function(){x["default"].set({title:gettext("Username history"),parent:this.props.profile.username})}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(changes)s username change.","Found %(changes)s username changes.",this.state.count);return interpolate(e,{changes:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("Your username was changed %(changes)s time.","Your username was changed %(changes)s times.",this.state.count);return interpolate(t,{changes:this.state.count},!0)}var a=ngettext("%(username)s's username was changed %(changes)s time.","%(username)s's username was changed %(changes)s times.",this.state.count);return interpolate(a,{username:this.props.profile.username,changes:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no username changes matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("No name changes have been recorded for your account."):interpolate(gettext("%(username)s's username was never changed."),{username:this.props.profile.username},!0)}},{key:"getMoreButton",value:function(){return this.state.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(d["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},interpolate(gettext("Show older (%(more)s)"),{more:this.state.more},!0))):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"profile-username-history"},u["default"].createElement("nav",{className:"toolbar"},u["default"].createElement("h3",{className:"toolbar-left"},this.getLabel()),u["default"].createElement(p["default"],{className:"toolbar-right",value:this.state.search,onChange:this.search,placeholder:gettext("Search history...")})),u["default"].createElement(h["default"],{isLoaded:this.state.isLoaded,emptyMessage:this.getEmptyMessage(),changes:this.props["username-history"]}),this.getMoreButton())}}]),t}(u["default"].Component);a["default"]=j},{"../../index":299,"../../reducers/username-history":359,"../../services/ajax":361,"../../services/page-title":369,"../../services/snackbar":372,"../../services/store":373,"../button":7,"../quick-search":194,"../username-history/root":277,react:"react"}],194:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.className?"form-search "+this.props.className:"form-search"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("input",{type:"text",className:"form-control",value:this.props.value,onChange:this.props.onChange,placeholder:this.props.placeholder||gettext("Search...")}),u["default"].createElement("span",{className:"material-icon"},"search"))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],195:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=e("./register.js"),p=n(f),m=e("../services/ajax"),h=n(m),b=e("../services/captcha"),v=n(b),_=e("../services/modal"),g=n(_),y=e("../services/snackbar"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.showRegisterForm=function(){"closed"===misago.get("SETTINGS").account_activation?E["default"].info(gettext("New registrations are currently disabled.")):a.state.isLoaded?g["default"].show(u["default"].createElement(p["default"],{criteria:a.state.criteria})):(a.setState({isLoading:!0}),Promise.all([v["default"].load(),h["default"].get(misago.get("AUTH_CRITERIA_API"))]).then(function(e){a.setState({isLoading:!1,isLoaded:!0,criteria:e[1]}),g["default"].show(u["default"].createElement(p["default"],{criteria:e[1]}))},function(){a.setState({isLoading:!1}),E["default"].error(gettext("Registration is currently unavailable due to an error."))}))},a.state={isLoading:!1,isLoaded:!1,criteria:null},a}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.className+(this.state.isLoading?" btn-loading":"")}},{key:"render",value:function(){return u["default"].createElement("button",{className:"btn "+this.getClassName(),disabled:this.state.isLoading,onClick:this.showRegisterForm,type:"button"},gettext("Register"),this.state.isLoading?u["default"].createElement(d["default"],null):null)}}]),t}(u["default"].Component);a["default"]=w},{"../services/ajax":361,"../services/captcha":363,"../services/modal":367,"../services/snackbar":372,"./loader":56,"./register.js":196,react:"react"}],196:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.RegisterComplete=a.RegisterForm=void 0;var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),c=e("react"),d=r(c),f=e("./button"),p=r(f),m=e("./form"),h=r(m),b=e("./form-group"),v=r(b),_=e("./password-strength"),g=r(_),y=e("./RegisterLegalFootnote"),E=r(y),w=e("./StartSocialAuth"),O=r(w),k=e(".."),N=r(k),x=e("../services/ajax"),j=r(x),P=e("../services/auth"),C=r(P),M=e("../services/captcha"),S=r(M),T=e("../services/modal"),L=r(T),A=e("../services/snackbar"),R=r(A),I=e("../utils/banned-page"),D=r(I),U=e("../utils/validators"),B=n(U),H=a.RegisterForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)),n=a.props.criteria,r=n.username,s=n.password,i=0;return s.forEach(function(e){"MinimumLengthValidator"===e.name&&(i=e.min_length)}),a.state={isLoading:!1,username:"",email:"",password:"",captcha:"",validators:{username:[B.usernameContent(),B.usernameMinLength(r.min_length),B.usernameMaxLength(r.max_length)],email:[B.email()],password:[B.passwordMinLength(i)],captcha:S["default"].validator()},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.isValid()||(R["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return j["default"].post(N["default"].get("USERS_API"),{username:this.state.username,email:this.state.email,password:this.state.password,captcha:this.state.captcha})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),e.__all__&&e.__all__.length>0?R["default"].error(e.__all__[0]):R["default"].error(gettext("Form contains errors."))):403===e.status&&e.ban?((0,D["default"])(e.ban),L["default"].hide()):R["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog modal-register",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Register"))),d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("input",{type:"type",style:{display:"none"}}),d["default"].createElement("input",{type:"password",style:{display:"none"}}),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(O["default"],{buttonClassName:"col-xs-12 col-sm-6",buttonLabel:gettext("Join with %(site)s"),formLabel:gettext("Or create forum account:")}),d["default"].createElement(v["default"],{label:gettext("Username"),"for":"id_username",validation:this.state.errors.username},d["default"].createElement("input",{type:"text",id:"id_username",className:"form-control","aria-describedby":"id_username_status",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username})),d["default"].createElement(v["default"],{label:gettext("E-mail"),"for":"id_email",validation:this.state.errors.email},d["default"].createElement("input",{type:"text",id:"id_email",className:"form-control","aria-describedby":"id_email_status",disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email})),d["default"].createElement(v["default"],{label:gettext("Password"),"for":"id_password",validation:this.state.errors.password,extra:d["default"].createElement(g["default"],{password:this.state.password,inputs:[this.state.username,this.state.email]})},d["default"].createElement("input",{type:"password",id:"id_password",className:"form-control","aria-describedby":"id_password_status",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password})),S["default"].component({form:this}),d["default"].createElement(E["default"],null)),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement(p["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Register account"))))))}}]),t}(h["default"]),z=a.RegisterComplete=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),u(t,[{key:"getLead",value:function(){return"user"===this.props.activation?gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in."):"admin"===this.props.activation?gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."):void 0}},{key:"getSubscript",value:function(){return"user"===this.props.activation?gettext("We have sent an e-mail to %(email)s with link that you have to click to activate your account."):"admin"===this.props.activation?gettext("We will send an e-mail to %(email)s when this takes place."):void 0}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog modal-message modal-register",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Registration complete"))),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement("div",{className:"message-icon"},d["default"].createElement("span",{className:"material-icon"},"info_outline")),d["default"].createElement("div",{className:"message-body"},d["default"].createElement("p",{className:"lead"},interpolate(this.getLead(),{username:this.props.username},!0)),d["default"].createElement("p",null,interpolate(this.getSubscript(),{email:this.props.email},!0)),d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))))}}]),t}(d["default"].Component),F=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.completeRegistration=function(e){"active"===e.activation?(L["default"].hide(),C["default"].signIn(e)):a.setState({complete:e})},a.state={complete:!1},a}return s(t,e),u(t,[{key:"render",value:function(){return this.state.complete?d["default"].createElement(z,{activation:this.state.complete.activation,email:this.state.complete.email,username:this.state.complete.username}):d["default"].createElement(H,i({callback:this.completeRegistration},this.props))}}]),t}(d["default"].Component);a["default"]=F},{"..":299,"../services/ajax":361,"../services/auth":362,"../services/captcha":363,"../services/modal":367,"../services/snackbar":372,"../utils/banned-page":375,"../utils/validators":389,"./RegisterLegalFootnote":1,"./StartSocialAuth":2,"./button":7,"./form":54,"./form-group":53,"./password-strength":100,react:"react"}],197:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LinkSent=a.RequestLinkForm=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../index"),f=r(d),p=e("./button"),m=r(p),h=e("./form"),b=r(h),v=e("../services/ajax"),_=r(v),g=e("../services/snackbar"),y=r(g),E=e("../utils/validators"),w=n(E),O=e("../utils/banned-page"),k=r(O),N=a.RequestLinkForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,email:"",validators:{email:[w.email()]}},a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(y["default"].error(gettext("Enter a valid email address.")),!1)}},{key:"send",value:function(){return _["default"].post(f["default"].get("SEND_ACTIVATION_API"),{email:this.state.email})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){["already_active","inactive_admin"].indexOf(e.code)>-1?y["default"].info(e.detail):403===e.status&&e.ban?(0,k["default"])(e.ban):y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-activation-link"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"form-group"},c["default"].createElement("div",{className:"control-input"},c["default"].createElement("input",{type:"text",className:"form-control",placeholder:gettext("Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),c["default"].createElement(m["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Send link"))))}}]),t}(b["default"]),x=a.LinkSent=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getMessage",value:function(){return interpolate(gettext("Activation link was sent to %(email)s"),{email:this.props.user.email},!0)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-activation-link well-done"},c["default"].createElement("div",{className:"done-message"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"check")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",null,this.getMessage())),c["default"].createElement("button",{className:"btn btn-primary btn-block",type:"button",onClick:this.props.callback},gettext("Request another link"))))}}]),t}(c["default"].Component),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.complete=function(e){a.setState({complete:e})},a.reset=function(){a.setState({complete:!1})},a.state={complete:!1},a}return s(t,e),i(t,[{key:"render",value:function(){return this.state.complete?c["default"].createElement(x,{user:this.state.complete,callback:this.reset}):c["default"].createElement(N,{callback:this.complete})}}]),t}(c["default"].Component);a["default"]=j},{"../index":299,"../services/ajax":361,"../services/snackbar":372,
-"../utils/banned-page":375,"../utils/validators":389,"./button":7,"./form":54,react:"react"}],198:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.AccountInactivePage=a.LinkSent=a.RequestResetForm=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("react-dom"),f=r(d),p=e("../index"),m=r(p),h=e("./button"),b=r(h),v=e("./form"),_=r(v),g=e("../services/ajax"),y=r(g),E=e("../services/snackbar"),w=r(E),O=e("../utils/validators"),k=n(O),N=e("../utils/banned-page"),x=r(N),j=a.RequestResetForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,email:"",validators:{email:[k.email()]}},a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(w["default"].error(gettext("Enter a valid email address.")),!1)}},{key:"send",value:function(){return y["default"].post(m["default"].get("SEND_PASSWORD_RESET_API"),{email:this.state.email})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){["inactive_user","inactive_admin"].indexOf(e.code)>-1?this.props.showInactivePage(e):403===e.status&&e.ban?(0,x["default"])(e.ban):w["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-password-reset"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"form-group"},c["default"].createElement("div",{className:"control-input"},c["default"].createElement("input",{type:"text",className:"form-control",placeholder:gettext("Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),c["default"].createElement(b["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Send link"))))}}]),t}(_["default"]),P=a.LinkSent=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getMessage",value:function(){return interpolate(gettext("Reset password link was sent to %(email)s"),{email:this.props.user.email},!0)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-password-reset well-done"},c["default"].createElement("div",{className:"done-message"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"check")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",null,this.getMessage())),c["default"].createElement("button",{type:"button",className:"btn btn-primary btn-block",onClick:this.props.callback},gettext("Request another link"))))}}]),t}(c["default"].Component),C=a.AccountInactivePage=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getActivateButton",value:function(){return"inactive_user"===this.props.activation?c["default"].createElement("p",null,c["default"].createElement("a",{href:m["default"].get("REQUEST_ACTIVATION_URL")},gettext("Activate your account."))):null}},{key:"render",value:function(){return c["default"].createElement("div",{className:"page page-message page-message-info page-forgotten-password-inactive"},c["default"].createElement("div",{className:"container"},c["default"].createElement("div",{className:"message-panel"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("Your account is inactive.")),c["default"].createElement("p",null,this.props.message),this.getActivateButton()))))}}]),t}(c["default"].Component),M=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.complete=function(e){a.setState({complete:e})},a.reset=function(){a.setState({complete:!1})},a.state={complete:!1},a}return s(t,e),i(t,[{key:"showInactivePage",value:function(e){f["default"].render(c["default"].createElement(C,{activation:e.code,message:e.detail}),document.getElementById("page-mount"))}},{key:"render",value:function(){return this.state.complete?c["default"].createElement(P,{callback:this.reset,user:this.state.complete}):c["default"].createElement(j,{callback:this.complete,showInactivePage:this.showInactivePage})}}]),t}(c["default"].Component);a["default"]=M},{"../index":299,"../services/ajax":361,"../services/snackbar":372,"../utils/banned-page":375,"../utils/validators":389,"./button":7,"./form":54,react:"react","react-dom":"react-dom"}],199:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.PasswordChangedPage=a.ResetPasswordForm=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-dom"),d=n(c),f=e("../index"),p=n(f),m=e("./button"),h=n(m),b=e("./form"),v=n(b),_=e("./sign-in.js"),g=n(_),y=e("../services/ajax"),E=n(y),w=e("../services/auth"),O=n(w),k=e("../services/modal"),N=n(k),x=e("../services/snackbar"),j=n(x),P=e("../utils/banned-page"),C=n(P),M=a.ResetPasswordForm=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,password:""},a}return l(t,e),s(t,[{key:"clean",value:function(){return!!this.state.password.trim().length||(j["default"].error(gettext("Enter new password.")),!1)}},{key:"send",value:function(){return E["default"].post(p["default"].get("CHANGE_PASSWORD_API"),{password:this.state.password})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){403===e.status&&e.ban?(0,C["default"])(e.ban):j["default"].apiError(e)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"well well-form well-form-reset-password"},u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{type:"password",className:"form-control",placeholder:gettext("Enter new password"),disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),u["default"].createElement(h["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Change password"))))}}]),t}(v["default"]),S=a.PasswordChangedPage=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getMessage",value:function(){return interpolate(gettext("%(username)s, your password has been changed successfully."),{username:this.props.user.username},!0)}},{key:"showSignIn",value:function(){N["default"].show(g["default"])}},{key:"render",value:function(){return u["default"].createElement("div",{className:"page page-message page-message-success page-forgotten-password-changed"},u["default"].createElement("div",{className:"container"},u["default"].createElement("div",{className:"message-panel"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},"check")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.getMessage()),u["default"].createElement("p",null,gettext("You will have to sign in using new password before continuing.")),u["default"].createElement("p",null,u["default"].createElement("button",{type:"button",className:"btn btn-primary",onClick:this.showSignIn},gettext("Sign in")))))))}}]),t}(u["default"].Component),T=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.complete=function(e){O["default"].softSignOut(),$('#hidden-login-form input[name="redirect_to"]').remove(),d["default"].render(u["default"].createElement(S,{user:e}),document.getElementById("page-mount"))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement(M,{callback:this.complete})}}]),t}(u["default"].Component);a["default"]=T},{"../index":299,"../services/ajax":361,"../services/auth":362,"../services/modal":367,"../services/snackbar":372,"../utils/banned-page":375,"./button":7,"./form":54,"./sign-in.js":208,react:"react","react-dom":"react-dom"}],200:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../.."),d=n(c),f=e("../form"),p=n(f),m=e("../../reducers/posts"),h=e("../../reducers/search"),b=e("../../reducers/users"),v=e("../../services/ajax"),_=n(v),g=e("../../services/snackbar"),y=n(g),E=e("../../services/store"),w=n(E),O=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onQueryChange=function(e){a.changeValue("query",e.target.value)},a.state={isLoading:!1,query:e.search.query},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){this.state.query.length&&this.handleSubmit()}},{key:"clean",value:function(){return!!this.state.query.trim().length||(y["default"].error(gettext("You have to enter search query.")),!1)}},{key:"send",value:function(){return w["default"].dispatch((0,h.update)({isLoading:!0})),_["default"].get(d["default"].get("SEARCH_API"),{q:this.state.query.trim()})}},{key:"handleSuccess",value:function(e){w["default"].dispatch((0,h.update)({query:this.state.query.trim(),isLoading:!1,providers:e})),e.forEach(function(e){"users"===e.id?w["default"].dispatch((0,b.hydrate)(e.results.results)):"threads"===e.id&&w["default"].dispatch((0,m.load)(e.results))})}},{key:"handleError",value:function(e){y["default"].apiError(e),w["default"].dispatch((0,h.update)({isLoading:!1}))}},{key:"render",value:function(){return u["default"].createElement("div",{className:"page-header-bg"},u["default"].createElement("div",{className:"page-header page-search-form"},u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"container"},u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-xs-12 col-md-3"},u["default"].createElement("h1",null,gettext("Search"))),u["default"].createElement("div",{className:"col-xs-12 col-md-9"},u["default"].createElement("div",{className:"row xs-margin-top sm-margin-top"},u["default"].createElement("div",{className:"col-xs-12 col-sm-8 col-md-9"},u["default"].createElement("div",{className:"form-group"},u["default"].createElement("input",{className:"form-control",disabled:this.props.search.isLoading||this.state.isLoading,onChange:this.onQueryChange,type:"text",value:this.state.query}))),u["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3"},u["default"].createElement("button",{className:"btn btn-primary btn-block btn-outline",disabled:this.props.search.isLoading||this.state.isLoading},gettext("Search"))))))))))}}]),t}(p["default"]);a["default"]=O},{"../..":299,"../../reducers/posts":350,"../../reducers/search":353,"../../reducers/users":360,"../../services/ajax":361,"../../services/snackbar":372,"../../services/store":373,"../form":54,react:"react"}],201:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{posts:e.posts,search:e.search,tick:e.tick.tick,user:e.auth.user,users:e.users}}Object.defineProperty(a,"__esModule",{value:!0}),a.select=r,a["default"]=function(e){return e.map(function(e){return{path:e.url,component:(0,o.connect)(r)(c[e.id]),provider:e}})};var o=e("react-redux"),l=e("./threads"),s=n(l),i=e("./users"),u=n(i),c={threads:s["default"],users:u["default"]}},{"./threads":204,"./users":206,"react-redux":"react-redux"}],202:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=null;if(e.search.providers.forEach(function(a){a.id===e.provider.id&&(t=a.time)}),null===t)return null;var a=gettext("Search took %(time)s s to complete");return l["default"].createElement("footer",{className:"search-footer"},l["default"].createElement("p",null,interpolate(a,{time:t},!0)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("div",{className:"page page-search"},l["default"].createElement(i["default"],{provider:e.provider,search:e.search}),l["default"].createElement("div",{className:"container"},l["default"].createElement("div",{className:"row"},l["default"].createElement("div",{className:"col-md-3"},l["default"].createElement(c["default"],{providers:e.search.providers})),l["default"].createElement("div",{className:"col-md-9"},e.children,l["default"].createElement(r,{provider:e.provider,search:e.search})))))},a.SearchTime=r;var o=e("react"),l=n(o),s=e("./form"),i=n(s),u=e("./sidenav"),c=n(u)},{"./form":200,"./sidenav":203,react:"react"}],203:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(!e.results)return null;var t=e.results.count;return t>1e6?t=Math.ceil(t/1e6)+"KK":t>1e3&&(t=Math.ceil(t/1e3)+"K"),l["default"].createElement("span",{className:"badge"},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("div",{className:"list-group nav-side"},e.providers.map(function(e){return l["default"].createElement(s.Link,{activeClassName:"active",className:"list-group-item",key:e.id,to:e.url},l["default"].createElement("span",{className:"material-icon"},e.icon),e.name,l["default"].createElement(r,{results:e.results}))}))},a.Badge=r;var o=e("react"),l=n(o),s=e("react-router")},{react:"react","react-router":"react-router"}],204:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.posts&&e.posts.count?e.children:e.query.length?s["default"].createElement("p",{className:"lead"},gettext("No threads matching search query have been found.")):s["default"].createElement("p",{className:"lead"},gettext("Enter at least two characters to search threads."))}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],{provider:e.route.provider,search:e.search},s["default"].createElement(r,{query:e.search.query,posts:e.posts},s["default"].createElement(d["default"],o({provider:e.route.provider,query:e.search.query},e.posts))))},a.Blankslate=r;var l=e("react"),s=n(l),i=e("../page"),u=n(i),c=e("./results"),d=n(c)},{"../page":202,"./results":205,react:"react"}],205:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LoadMore=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return u["default"].createElement("div",null,u["default"].createElement(d["default"],{isReady:!0,posts:e.results}),u["default"].createElement(O,e))};var i=e("react"),u=n(i),c=e("../../post-feed"),d=n(c),f=e("../../button"),p=n(f),m=e("../../misago-markup"),h=(n(m),e("../../../reducers/posts")),b=e("../../../reducers/search"),v=e("../../../services/ajax"),_=n(v),g=e("../../../services/snackbar"),y=n(g),E=e("../../../services/store"),w=n(E),O=a.LoadMore=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){w["default"].dispatch((0,h.update)({isBusy:!0})),_["default"].get(n.props.provider.api,{q:n.props.query,page:n.props.next}).then(function(e){e.forEach(function(e){"threads"===e.id&&(w["default"].dispatch((0,h.append)(e.results)),w["default"].dispatch((0,b.updateProvider)(e)))}),w["default"].dispatch((0,h.update)({isBusy:!1}))},function(e){y["default"].apiError(e),w["default"].dispatch((0,h.update)({isBusy:!1}))})},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(p["default"],{className:"btn btn-default btn-outline",loading:this.props.isBusy,onClick:this.onClick},gettext("Show more"))):null}}]),t}(u["default"].Component)},{"../../../reducers/posts":350,"../../../reducers/search":353,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,"../../button":7,"../../misago-markup":58,"../../post-feed":119,react:"react"}],206:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.users.length?e.children:e.query.length?l["default"].createElement("p",{className:"lead"},gettext("No users matching search query have been found.")):l["default"].createElement("p",{className:"lead"},gettext("Enter at least two characters to search users."))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement(i["default"],{provider:e.route.provider,search:e.search},l["default"].createElement(r,{query:e.search.query,users:e.users},l["default"].createElement(c["default"],{cols:3,isReady:!0,users:e.users})))},a.Blankslate=r;var o=e("react"),l=n(o),s=e("../page"),i=n(s),u=e("../../users-list"),c=n(u)},{"../../users-list":281,"../page":202,react:"react"}],207:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.icon;return t?c["default"].createElement("span",{className:"material-icon"},t):null}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Icon=s;var u=e("react"),c=n(u),d=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.change=function(e){return function(){n.props.onChange({target:{value:e}})}},l=a,o(n,l)}return l(t,e),i(t,[{key:"getChoice",value:function(){var e=this,t=null;return this.props.choices.map(function(a){a.value===e.props.value&&(t=a)}),t}},{key:"getIcon",value:function(){return this.getChoice().icon}},{key:"getLabel",value:function(){return this.getChoice().label}},{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"btn-group btn-select-group"},c["default"].createElement("button",{type:"button",className:"btn btn-select dropdown-toggle",id:this.props.id||null,"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false","aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},c["default"].createElement(s,{icon:this.getIcon()}),this.getLabel()),c["default"].createElement("ul",{className:"dropdown-menu"},this.props.choices.map(function(t,a){return c["default"].createElement("li",{key:a},c["default"].createElement("button",{type:"button",className:"btn-link",onClick:e.change(t.value)},c["default"].createElement(s,{icon:t.icon}),t.label))})))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],208:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../index"),d=n(c),f=e("./button"),p=n(f),m=e("./form"),h=n(m),b=e("./StartSocialAuth"),v=n(b),_=e("../services/ajax"),g=n(_),y=e("../services/modal"),E=n(y),w=e("../services/snackbar"),O=n(w),k=e("../utils/banned-page"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,showActivation:!1,username:"",password:"",validators:{username:[],password:[]}},a}return l(t,e),s(t,[{key:"clean",value:function(){return!!this.isValid()||(O["default"].error(gettext("Fill out both fields.")),!1)}},{key:"send",value:function(){return g["default"].post(d["default"].get("AUTH_API"),{username:this.state.username,password:this.state.password})}},{key:"handleSuccess",value:function(){var e=$("#hidden-login-form");e.append('<input type="text" name="username" />'),e.append('<input type="password" name="password" />'),e.find('input[type="hidden"]').val(g["default"].getCsrfToken()),e.find('input[name="redirect_to"]').val(window.location.pathname),e.find('input[name="username"]').val(this.state.username),e.find('input[name="password"]').val(this.state.password),e.submit(),this.setState({isLoading:!0})}},{key:"handleError",value:function(e){400===e.status?"inactive_admin"===e.code?O["default"].info(e.detail):"inactive_user"===e.code?(O["default"].info(e.detail),this.setState({showActivation:!0})):"banned"===e.code?((0,N["default"])(e.detail),E["default"].hide()):O["default"].error(e.detail):403===e.status&&e.ban?((0,N["default"])(e.ban),E["default"].hide()):O["default"].apiError(e)}},{key:"getActivationButton",value:function(){return this.state.showActivation?u["default"].createElement("a",{className:"btn btn-success btn-block",href:d["default"].get("REQUEST_ACTIVATION_URL")},gettext("Activate account")):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-dialog modal-sm modal-sign-in",role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Sign in"))),u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(v["default"],{buttonLabel:gettext("Sign in with %(site)s"),formLabel:gettext("Or use your forum account:"),labelClassName:"text-center"}),u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_username",onChange:this.bindInput("username"),placeholder:gettext("Username or e-mail"),type:"text",value:this.state.username}))),u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_password",onChange:this.bindInput("password"),placeholder:gettext("Password"),type:"password",value:this.state.password})))),u["default"].createElement("div",{className:"modal-footer"},this.getActivationButton(),u["default"].createElement(p["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Sign in")),u["default"].createElement("a",{className:"btn btn-default btn-block",href:d["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Forgot password?"))))))}}]),t}(h["default"]);a["default"]=x},{"../index":299,"../services/ajax":361,"../services/modal":367,"../services/snackbar":372,"../utils/banned-page":375,"./StartSocialAuth":2,"./button":7,"./form":54,react:"react"}],209:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.snackbar}Object.defineProperty(a,"__esModule",{value:!0}),a.Snackbar=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d={info:"alert-info",success:"alert-success",warning:"alert-warning",error:"alert-danger"};a.Snackbar=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getSnackbarClass",value:function(){var e="alerts-snackbar";return e+=this.props.isVisible?" in":" out"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getSnackbarClass()},c["default"].createElement("p",{className:"alert "+d[this.props.type]},this.props.message))}}]),t}(c["default"].Component)},{react:"react"}],210:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e("./header"),s=n(l),i=e("../.."),u=n(i),c=function(e){var t=e.activation,a=e.backend_name,n=e.username,r="",l="";return l="user"===t?gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in."):"admin"===t?gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."):gettext("%(username)s, your account has been created and you has been signed in to it."),r="active"===t?"check":"info_outline",o["default"].createElement("div",{className:"page page-social-auth page-social-sauth-register"},o["default"].createElement(s["default"],{backendName:a}),o["default"].createElement("div",{className:"container"},o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Registration completed!"))),o["default"].createElement("div",{className:"panel-body panel-message-body"},o["default"].createElement("div",{className:"message-icon"},o["default"].createElement("span",{className:"material-icon"},r)),o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",{className:"lead"},interpolate(l,{username:n},!0)),o["default"].createElement("p",{className:"help-block"},o["default"].createElement("a",{className:"btn btn-default",href:u["default"].get("MISAGO_PATH")},gettext("Return to forum index"))))))))))};a["default"]=c},{"../..":299,"./header":211,react:"react"}],211:[function(e,t,a){
-"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=function(e){var t=e.backendName,a=gettext("Sign in with %(backend)s"),n=interpolate(a,{backend:t},!0);return o["default"].createElement("div",{className:"page-header-bg"},o["default"].createElement("div",{className:"page-header"},o["default"].createElement("div",{className:"container"},o["default"].createElement("h1",null,n))))};a["default"]=l},{react:"react"}],212:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./register"),d=n(c),f=e("./complete"),p=n(f),m=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleRegistrationComplete=function(e){var t=e.activation,n=e.email,r=e.step,o=e.username;a.setState({activation:t,email:n,step:r,username:o})},a.state={step:e.step,activation:e.activation||"",email:e.email||"",username:e.username||""},a}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props,t=e.backend_name,a=e.url,n=this.state,r=n.activation,o=n.email,l=n.step,s=n.username;return"register"===l?u["default"].createElement(d["default"],{backend_name:t,email:o,url:a,username:s,onRegistrationComplete:this.handleRegistrationComplete}):u["default"].createElement(p["default"],{activation:r,backend_name:t,email:o,url:a,username:s})}}]),t}(u["default"].Component);a["default"]=m},{"./complete":210,"./register":213,react:"react"}],213:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../RegisterLegalFootnote"),f=r(d),p=e("../button"),m=r(p),h=e("../form"),b=r(h),v=e("../form-group"),_=r(v),g=e("../../services/ajax"),y=r(g),E=e("../../services/snackbar"),w=r(E),O=e("../../utils/validators"),k=n(O),N=e("./header"),x=r(N),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={email:e.email||"",emailProtected:!!e.email,username:e.username||"",errors:{},validators:{email:[k.email()],username:[k.usernameContent()]},isLoading:!1},a}return s(t,e),i(t,[{key:"clean",value:function(){var e=(this.validate(),[this.state.email.trim().length,this.state.username.trim().length]);return e.indexOf(0)===-1||(w["default"].error(gettext("Fill out all fields.")),!1)}},{key:"send",value:function(){return y["default"].post(this.props.url,{email:this.state.email,username:this.state.username})}},{key:"handleSuccess",value:function(e){onRegistrationComplete(e)}},{key:"handleError",value:function(e){if(200===e.status){var t=this.props.onRegistrationComplete,a=this.state.username;t({activation:"active",step:"done",username:a})}else if(400===e.status){var n={errors:e};e.email&&(n.emailProtected=!1),this.setState(n)}else w["default"].apiError(e)}},{key:"render",value:function(){var e=this.props.backend_name,t=this.state,a=t.email,n=t.emailProtected,r=t.username,o=t.isLoading,l=null;if(n){var s=gettext("Your e-mail address has been verified by %(backend)s.");l=interpolate(s,{backend:e},!0)}return c["default"].createElement("div",{className:"page page-social-auth page-social-sauth-register"},c["default"].createElement(x["default"],{backendName:e}),c["default"].createElement("div",{className:"container"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Complete your details"))),c["default"].createElement("div",{className:"panel-body"},c["default"].createElement(_["default"],{"for":"id_username",label:gettext("Username"),validation:this.state.errors.username},c["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:o,onChange:this.bindInput("username"),value:r})),c["default"].createElement(_["default"],{"for":"id_email",label:gettext("E-mail address"),helpText:l,validation:n?null:this.state.errors.email},c["default"].createElement("input",{type:"email",id:"id_email",className:"form-control",disabled:o||n,onChange:this.bindInput("email"),value:a})),c["default"].createElement(f["default"],null)),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement(m["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Sign in")))))))))}}]),t}(b["default"]);a["default"]=j},{"../../services/ajax":361,"../../services/snackbar":372,"../../utils/validators":389,"../RegisterLegalFootnote":1,"../button":7,"../form":54,"../form-group":53,"./header":211,react:"react"}],214:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("li",null,s["default"].createElement("a",{href:e.node.url.index},e.node.name))}function o(e){var t=e.path[e.path.length-1];return s["default"].createElement("a",{href:t.url.index,className:"go-back-sm visible-xs-block"},s["default"].createElement("span",{className:"material-icon"},"chevron_left"),t.name)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return s["default"].createElement("div",{className:"page-breadcrumbs"},s["default"].createElement("div",{className:"container"},s["default"].createElement("ol",{className:"breadcrumb hidden-xs"},e.path.map(function(e){return s["default"].createElement(r,{key:e.id,node:e})})),s["default"].createElement(o,e)))},a.Breadcrumb=r,a.GoBack=o;var l=e("react"),s=n(l)},{react:"react"}],215:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return f["default"].createElement("div",{className:e.isSingle?"col-xs-12":"col-xs-6"},f["default"].createElement("div",{className:"btn-group btn-group-justified"},f["default"].createElement("div",{className:"btn-group"},f["default"].createElement("button",{"aria-expanded":"false","aria-haspopup":"true",className:"btn btn-default btn-outline dropdown-toggle","data-toggle":"dropdown",disabled:e.thread.isBusy,type:"button"},f["default"].createElement("span",{className:"material-icon"},"settings"),f["default"].createElement("span",{className:e.isSingle?"":"hidden-sm"},gettext("Moderation"))),f["default"].createElement(h.ModerationControls,{posts:e.posts,thread:e.thread,user:e.user}))))}Object.defineProperty(a,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Moderation=i;var d=e("react"),f=r(d),p=e("./breadcrumbs"),m=r(p),h=e("../moderation/thread"),b=e("./stats"),v=r(b),_=e("../../form"),g=r(_),y=e("../../posting/utils/validators"),E=e("../../../services/ajax"),w=r(E),O=e("../../../services/snackbar"),k=r(O),N=e("../../../services/store"),x=r(N),j=e("../../../reducers/thread"),P=n(j),C=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onChange=function(e){a.changeValue("title",e.target.value)},a.onEdit=function(){a.setState({isEditing:!0})},a.onCancel=function(){a.setState({title:a.props.thread.title,isEditing:!1})},a.state={isEditing:!1,isLoading:!1,title:e.thread.title,validators:{title:(0,y.getTitleValidators)()},errors:{}},a}return s(t,e),c(t,[{key:"clean",value:function(){if(!this.state.title.trim().length)return k["default"].error(gettext("You have to enter thread title.")),!1;var e=this.validate();return!e.title||(k["default"].error(e.title[0]),!1)}},{key:"send",value:function(){return w["default"].patch(this.props.thread.api.index,[{op:"replace",path:"title",value:this.state.title}])}},{key:"handleSuccess",value:function(e){x["default"].dispatch(P.update(e)),this.setState({isEditing:!1})}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail[0]):k["default"].apiError(e)}},{key:"render",value:function(){var e=this.props,t=e.thread,a=e.user,n=!!a.id&&(0,h.isModerationVisible)(t);return this.state.isEditing?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row xs-margin-top title-edit-form"},f["default"].createElement("form",{onSubmit:this.handleSubmit},f["default"].createElement("div",{className:"col-sm-6 col-md-6"},f["default"].createElement("input",{className:"form-control",type:"text",value:this.state.title,onChange:this.onChange})),f["default"].createElement("div",{className:"col-sm-6 col-md-4"},f["default"].createElement("div",{className:"row xs-margin-top-half sm-margin-top-no md-margin-top-no"},f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-primary btn-block btn-outline",disabled:this.state.isLoading,title:gettext("Change title")},gettext("Save changes"))),f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",disabled:this.state.isLoading,onClick:this.onCancel,title:gettext("Cancel"),type:"button"},gettext("Cancel")))))))),f["default"].createElement(v["default"],{thread:t})):a.id&&t.acl.can_edit?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row"},f["default"].createElement("div",{className:n?"col-sm-9 col-md-8":"col-sm-10 col-md-10"},f["default"].createElement("h1",null,t.title)),f["default"].createElement("div",{className:n?"col-sm-3 col-md-4":"col-sm-3 col-md-2"},f["default"].createElement("div",{className:"row xs-margin-top md-margin-top-no"},f["default"].createElement("div",{className:n?"col-xs-6":"col-xs-12"},f["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",onClick:this.onEdit,title:gettext("Edit title"),type:"button"},f["default"].createElement("span",{className:"material-icon"},"edit"),f["default"].createElement("span",{className:"hidden-sm"},gettext("Edit")))),n&&f["default"].createElement(i,this.props))))),f["default"].createElement(v["default"],{thread:t})):n?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row"},f["default"].createElement("div",{className:"col-sm-9 col-md-10"},f["default"].createElement("h1",null,t.title)),f["default"].createElement("div",{className:"col-sm-3 col-md-2"},f["default"].createElement("div",{className:"row xs-margin-top md-margin-top-no"},f["default"].createElement(i,u({isSingle:!0},this.props)))))),f["default"].createElement(v["default"],{thread:t})):f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("h1",null,t.title)),f["default"].createElement(v["default"],{thread:t}))}}]),t}(g["default"]);a["default"]=C},{"../../../reducers/thread":356,"../../../services/ajax":361,"../../../services/snackbar":372,"../../../services/store":373,"../../form":54,"../../posting/utils/validators":141,"../moderation/thread":224,"./breadcrumbs":214,"./stats":216,react:"react"}],216:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return 2==e.thread.weight?d["default"].createElement("li",{className:"thread-pinned-globally"},d["default"].createElement("span",{className:"material-icon"},"bookmark"),d["default"].createElement("span",{className:"icon-legend"},gettext("Pinned globally"))):1==e.thread.weight?d["default"].createElement("li",{className:"thread-pinned-locally"},d["default"].createElement("span",{className:"material-icon"},"bookmark_border"),d["default"].createElement("span",{className:"icon-legend"},gettext("Pinned locally"))):null}function o(e){return e.thread.is_unapproved?d["default"].createElement("li",{className:"thread-unapproved"},d["default"].createElement("span",{className:"material-icon"},"remove_circle"),d["default"].createElement("span",{className:"icon-legend"},gettext("Unapproved"))):e.thread.has_unapproved_posts?d["default"].createElement("li",{className:"thread-unapproved-posts"},d["default"].createElement("span",{className:"material-icon"},"remove_circle_outline"),d["default"].createElement("span",{className:"icon-legend"},gettext("Unapproved posts"))):null}function l(e){return e.thread.is_hidden?d["default"].createElement("li",{className:"thread-hidden"},d["default"].createElement("span",{className:"material-icon"},"visibility_off"),d["default"].createElement("span",{className:"icon-legend"},gettext("Hidden"))):null}function s(e){return e.thread.is_closed?d["default"].createElement("li",{className:"thread-closed"},d["default"].createElement("span",{className:"material-icon"},"lock_outline"),d["default"].createElement("span",{className:"icon-legend"},gettext("Closed"))):null}function i(e){var t=ngettext("%(replies)s reply","%(replies)s replies",e.thread.replies),a=interpolate(t,{replies:e.thread.replies},!0);return d["default"].createElement("li",{className:"thread-replies"},d["default"].createElement("span",{className:"material-icon"},"forum"),d["default"].createElement("span",{className:"icon-legend"},a))}function u(e){var t=null;t=e.thread.url.last_poster?interpolate(m,{url:(0,p["default"])(e.thread.url.last_poster),user:(0,p["default"])(e.thread.last_poster_name)},!0):interpolate(h,{user:(0,p["default"])(e.thread.last_poster_name)},!0);var a=interpolate(b,{absolute:(0,p["default"])(e.thread.last_post_on.format("LLL")),relative:(0,p["default"])(e.thread.last_post_on.fromNow())},!0),n=interpolate((0,p["default"])(gettext("last reply by %(user)s %(date)s")),{date:a,user:t},!0);return d["default"].createElement("li",{className:"thread-last-reply",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a.Weight=r,a.Unapproved=o,a.IsHidden=l,a.IsClosed=s,a.Replies=i,a.LastReply=u,a["default"]=function(e){return d["default"].createElement("div",{className:"header-stats"},d["default"].createElement("div",{className:"container"},d["default"].createElement("ul",{className:"list-inline"},d["default"].createElement(r,{thread:e.thread}),d["default"].createElement(o,{thread:e.thread}),d["default"].createElement(l,{thread:e.thread}),d["default"].createElement(s,{thread:e.thread}),d["default"].createElement(i,{thread:e.thread}),d["default"].createElement(u,{thread:e.thread}))))};var c=e("react"),d=n(c),f=e("../../../utils/escape-html"),p=n(f),m='<a href="%(url)s" class="poster-title">%(user)s</a>',h='<span class="poster-title">%(user)s</span>',b='<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'},{"../../../utils/escape-html":379,react:"react"}],217:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){var t=e.selection,a=[{op:"replace",path:"is-unapproved",value:!1}],n=t.map(function(e){return{id:e.id,is_unapproved:!1}}),r=t.map(function(e){return{id:e.id,is_unapproved:e.is_unapproved}});c(e,a,n,r)}function l(e){var t=e.selection,a=[{op:"replace",path:"is-protected",value:!0}],n=t.map(function(e){return{id:e.id,is_protected:!0}}),r=t.map(function(e){return{id:e.id,is_protected:e.is_protected}});c(e,a,n,r)}function s(e){var t=e.selection,a=[{op:"replace",path:"is-protected",value:!1}],n=t.map(function(e){return{id:e.id,is_protected:!1}}),r=t.map(function(e){return{id:e.id,is_protected:e.is_protected}});c(e,a,n,r)}function i(e){var t=e.selection,a=[{op:"replace",path:"is-hidden",value:!0}],n=t.map(function(t){return{id:t.id,is_hidden:!0,hidden_on:(0,m["default"])(),hidden_by_name:e.user.username,url:Object.assign(t.url,{hidden_by:e.user.url})}}),r=t.map(function(e){return{id:e.id,is_hidden:e.is_hidden,hidden_on:e.hidden_on,hidden_by_name:e.hidden_by_name,url:e.url}});c(e,a,n,r)}function u(e){var t=e.selection,a=[{op:"replace",path:"is-hidden",value:!1}],n=t.map(function(t){return{id:t.id,is_hidden:!1,hidden_on:(0,m["default"])(),hidden_by_name:e.user.username,url:Object.assign(t.url,{hidden_by:e.user.url})}}),r=t.map(function(e){return{id:e.id,is_hidden:e.is_hidden,hidden_on:e.hidden_on,hidden_by_name:e.hidden_by_name,url:e.url}});c(e,a,n,r)}function c(e,t,a,n){var r=e.selection,o=e.thread;a.forEach(function(e){_.patch(e,e)}),P["default"].dispatch(y.deselectAll());var l={ops:t,ids:r.map(function(e){return e.id})};w["default"].patch(o.api.posts.index,l).then(function(e){e.forEach(function(e){P["default"].dispatch(_.patch(e,e))})},function(e){if(400!==e.status)return n.forEach(function(e){P["default"].dispatch(_.patch(e,e))}),x["default"].apiError(e);var t=[],a=[];e.forEach(function(e){e.detail?(t.push(e),a.push(e.id)):P["default"].dispatch(_.patch(e,e)),n.forEach(function(e){a.indexOf(e)!==-1&&P["default"].dispatch(_.patch(e,e))})});var o={};r.forEach(function(e){o[e.id]=e}),k["default"].show(b["default"].createElement(M["default"],{errors:t,posts:o}))})}function d(e){var t=confirm(gettext("Are you sure you want to merge selected posts? This action is not reversible!"));t&&(e.selection.slice(1).map(function(e){P["default"].dispatch(_.patch(e,{isDeleted:!0}))}),w["default"].post(e.thread.api.posts.merge,{posts:e.selection.map(function(e){return e.id})}).then(function(e){P["default"].dispatch(_.patch(e,_.hydrate(e)))},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),e.selection.slice(1).map(function(e){P["default"].dispatch(_.patch(e,{isDeleted:!1}))})}),P["default"].dispatch(y.deselectAll()))}function f(e){var t=confirm(gettext("Are you sure you want to delete selected posts? This action is not reversible!"));if(t){e.selection.map(function(e){P["default"].dispatch(_.patch(e,{isDeleted:!0}))});var a=e.selection.map(function(e){return e.id});w["default"]["delete"](e.thread.api.posts.index,a).then(function(){},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),e.selection.map(function(e){P["default"].dispatch(_.patch(e,{isDeleted:!1}))})}),P["default"].dispatch(y.deselectAll())}}Object.defineProperty(a,"__esModule",{value:!0}),a.approve=o,a.protect=l,a.unprotect=s,a.hide=i,a.unhide=u,a.patch=c,a.merge=d,a.remove=f;var p=e("moment"),m=r(p),h=e("react"),b=r(h),v=e("../../../../reducers/post"),_=n(v),g=e("../../../../reducers/posts"),y=n(g),E=e("../../../../services/ajax"),w=r(E),O=e("../../../../services/modal"),k=r(O),N=e("../../../../services/snackbar"),x=r(N),j=e("../../../../services/store"),P=r(j),C=e("./errors-list"),M=r(C)},{"../../../../reducers/post":349,"../../../../reducers/posts":350,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"./errors-list":219,moment:"moment",react:"react"}],218:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=a.Unprotect=a.Protect=a.Split=a.Move=a.Merge=a.Approve=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return c["default"].createElement("ul",{className:"dropdown-menu"},c["default"].createElement(g,e),c["default"].createElement(y,e),c["default"].createElement(E,e),c["default"].createElement(w,e),c["default"].createElement(O,e),c["default"].createElement(k,e),c["default"].createElement(x,e),c["default"].createElement(N,e),c["default"].createElement(j,e))};var u=e("react"),c=r(u),d=e("../../../../services/modal"),f=r(d),p=e("./actions"),m=n(p),h=e("./move"),b=r(h),v=e("./split"),_=r(v),g=a.Approve=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.approve(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_approve&&e.is_unapproved});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}}]),t}(c["default"].Component),y=a.Merge=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.merge(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.length>1&&this.props.selection.find(function(e){return e.acl.can_merge});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge"))):null}}]),t}(c["default"].Component),E=a.Move=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(b["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_move});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}}]),t}(c["default"].Component),w=a.Split=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(_["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_move});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"call_split"),gettext("Split"))):null}}]),t}(c["default"].Component),O=a.Protect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.protect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return!e.is_protected&&e.acl.can_protect});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Protect"))):null}}]),t}(c["default"].Component),k=a.Unprotect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.unprotect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.is_protected&&e.acl.can_protect});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Unprotect"))):null}}]),t}(c["default"].Component),N=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.hide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_hide&&!e.is_hidden});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}}]),t}(c["default"].Component),x=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.unhide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_unhide&&e.is_hidden});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}}]),t}(c["default"].Component),j=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.remove(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_delete});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}}]),t}(c["default"].Component)},{"../../../../services/modal":367,"./actions":217,"./move":221,"./split":222,react:"react"}],219:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.errors,a=e.post,n=interpolate(gettext("%(username)s on %(posted_on)s"),{posted_on:a.posted_on.format("LL, LT"),username:a.poster_name},!0);return l["default"].createElement("li",null,l["default"].createElement("h5",null,n,":"),t.map(function(e,t){return l["default"].createElement("p",{key:t},e)}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.errors,a=e.posts;return l["default"].createElement("div",{className:"modal-dialog",role:"document"},l["default"].createElement("div",{className:"modal-content"},l["default"].createElement("div",{className:"modal-header"},l["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},l["default"].createElement("span",{"aria-hidden":"true"},"×")),l["default"].createElement("h4",{className:"modal-title"},gettext("Moderation"))),l["default"].createElement("div",{className:"modal-body"},l["default"].createElement("p",{className:"lead"},gettext("One or more posts could not be changed:")),l["default"].createElement("ul",{className:"list-unstyled list-errored-items"},t.map(function(e){return l["default"].createElement(r,{errors:e.detail,key:e.id,post:a[e.id]})})))))},a.PostErrors=r;var o=e("react"),l=n(o)},{react:"react"}],220:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(e.acl.can_merge_posts&&t.length>1)return!0;var a=!1;return t.forEach(function(e){if(!e.is_event){var t=e.acl.can_approve&&e.is_unapproved||e.acl.can_delete||!e.is_hidden&&e.acl.can_hide||e.acl.can_move||e.acl.can_merge||e.acl.can_protect||e.is_hidden&&e.acl.can_unhide||e.acl.can_unprotect;t&&(a=!0)}}),a}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){
-for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){if(!e.user.id||!r(e.thread,e.posts.results))return null;var t=e.posts.results.filter(function(e){return e.isSelected});return s["default"].createElement("div",{className:"dropup"},s["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",disabled:!t.length,type:"button"},gettext("Posts options")),s["default"].createElement(u["default"],o({selection:t},e)))},a.isVisible=r;var l=e("react"),s=n(l),i=e("./dropdown"),u=n(i)},{"./dropdown":218,react:"react"}],221:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Move posts")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../button"),p=(r(f),e("../../../form")),m=r(p),h=e("../../../form-group"),b=r(h),v=e("../../../../reducers/post"),_=n(v),g=e("../../../../services/ajax"),y=r(g),E=e("../../../../services/modal"),w=r(E),O=e("../../../../services/snackbar"),k=r(O),N=e("../../../../services/store"),x=r(N),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(k["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return y["default"].post(this.props.thread.api.posts.move,{new_thread:this.state.url,posts:this.props.selection.map(function(e){return e.id})})}},{key:"handleSuccess",value:function(e){this.props.selection.forEach(function(e){x["default"].dispatch(_.patch(e,{isDeleted:!0}))}),w["default"].hide(),k["default"].success(gettext("Selected posts were moved to the other thread."))}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail):k["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(b["default"],{"for":"id_url",label:gettext("Link to thread you want to move posts to")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading},gettext("Move posts"))))))}}]),t}(m["default"]);a["default"]=j},{"../../../../reducers/post":349,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../button":7,"../../../form":54,"../../../form-group":53,react:"react"}],222:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement(k["default"],null))}function u(e){return m["default"].createElement(c,{className:"modal-dialog modal-message"},m["default"].createElement("div",{className:"message-icon"},m["default"].createElement("span",{className:"material-icon"},"info_outline")),m["default"].createElement("div",{className:"message-body"},m["default"].createElement("p",{className:"lead"},gettext("You can't move selected posts at the moment.")),m["default"].createElement("p",null,e.message),m["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}function c(e){return m["default"].createElement("div",{className:e.className,role:"document"},m["default"].createElement("div",{className:"modal-content"},m["default"].createElement("div",{className:"modal-header"},m["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},m["default"].createElement("span",{"aria-hidden":"true"},"×")),m["default"].createElement("h4",{className:"modal-title"},gettext("Split posts into new thread"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0}),a.ModerationForm=a.PostingConfig=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return m["default"].createElement(B,f({},e,{Form:H}))},a.Loader=i,a.Error=u,a.Modal=c;var p=e("react"),m=r(p),h=e("../../../button"),b=r(h),v=e("../../../form"),_=r(v),g=e("../../../form-group"),y=r(g),E=e("../../../category-select"),w=r(E),O=e("../../../modal-loader"),k=r(O),N=e("../../../select"),x=r(N),j=e("../../../../reducers/post"),P=n(j),C=e("../../../../services/ajax"),M=r(C),S=e("../../../../services/modal"),T=r(S),L=e("../../../../services/snackbar"),A=r(L),R=e("../../../../services/store"),I=r(R),D=e("../../../../utils/validators"),U=n(D),B=a.PostingConfig=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isError:!1,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;M["default"].get(misago.get("THREAD_EDITOR_API")).then(function(t){var a=t.map(function(e){return Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id,post:e.post})});e.setState({isLoaded:!0,categories:a})},function(t){e.setState({isError:t.detail})})}},{key:"render",value:function(){return this.state.isError?m["default"].createElement(u,{message:this.state.isError}):this.state.isLoaded?m["default"].createElement(H,f({},this.props,{categories:this.state.categories})):m["default"].createElement(i,null)}}]),t}(m["default"].Component),H=a.ModerationForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,categories:e.categories,weight:0,is_hidden:0,is_closed:!1,validators:{title:[U.required()]},errors:{}},a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a.acl={},a.props.categories.forEach(function(e){e.post&&(a.state.category||(a.state.category=e.id),a.acl[e.id]={can_pin_threads:e.post.pin,can_close_threads:e.post.close,can_hide_threads:e.post.hide})}),a}return s(t,e),d(t,[{key:"clean",value:function(){return!!this.isValid()||(A["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return M["default"].post(this.props.thread.api.posts.split,{title:this.state.title,category:this.state.category,weight:this.state.weight,is_hidden:this.state.is_hidden,is_closed:this.state.is_closed,posts:this.props.selection.map(function(e){return e.id})})}},{key:"handleSuccess",value:function(e){this.props.selection.forEach(function(e){I["default"].dispatch(P.patch(e,{isDeleted:!0}))}),T["default"].hide(),A["default"].success(gettext("Selected posts were split into new thread."))}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),A["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?T["default"].show(m["default"].createElement(ErrorsModal,{errors:e})):A["default"].apiError(e)}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?m["default"].createElement(y["default"],{label:gettext("Thread weight"),"for":"id_weight",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?m["default"].createElement(y["default"],{label:gettext("Hide thread"),"for":"id_is_hidden",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?m["default"].createElement(y["default"],{label:gettext("Close thread"),"for":"id_is_closed",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"render",value:function(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement("form",{onSubmit:this.handleSubmit},m["default"].createElement("div",{className:"modal-body"},m["default"].createElement(y["default"],{label:gettext("Thread title"),"for":"id_title",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.title},m["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),m["default"].createElement("div",{className:"clearfix"}),m["default"].createElement(y["default"],{label:gettext("Category"),"for":"id_category",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.category},m["default"].createElement(w["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.state.categories})),m["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),m["default"].createElement("div",{className:"modal-footer"},m["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),m["default"].createElement(b["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Split posts")))))}}]),t}(_["default"])},{"../../../../reducers/post":349,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../../utils/validators":389,"../../../button":7,"../../../category-select":20,"../../../form":54,"../../../form-group":53,"../../../modal-loader":59,"../../../select":207,react:"react"}],223:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./merge"),f=r(d),p=e("./move"),m=r(p),h=e("../../../../reducers/thread"),b=n(h),v=e("../../../../services/ajax"),_=r(v),g=e("../../../../services/modal"),y=r(g),E=e("../../../../services/snackbar"),w=r(E),O=e("../../../../services/store"),k=r(O),N=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.callApi=function(e,t){k["default"].dispatch(b.busy()),e.push({op:"add",path:"acl",value:!0}),_["default"].patch(n.props.thread.api.index,e).then(function(e){k["default"].dispatch(b.update(e)),k["default"].dispatch(b.release()),w["default"].success(t)},function(e){k["default"].dispatch(b.release()),400===e.status?w["default"].error(e.detail[0]):w["default"].apiError(e)})},n.pinGlobally=function(){n.callApi([{op:"replace",path:"weight",value:2}],gettext("Thread has been pinned globally."))},n.pinLocally=function(){n.callApi([{op:"replace",path:"weight",value:1}],gettext("Thread has been pinned locally."))},n.unpin=function(){n.callApi([{op:"replace",path:"weight",value:0}],gettext("Thread has been unpinned."))},n.approve=function(){n.callApi([{op:"replace",path:"is-unapproved",value:!1}],gettext("Thread has been approved."))},n.open=function(){n.callApi([{op:"replace",path:"is-closed",value:!1}],gettext("Thread has been opened."))},n.close=function(){n.callApi([{op:"replace",path:"is-closed",value:!0}],gettext("Thread has been closed."))},n.unhide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!1}],gettext("Thread has been made visible."))},n.hide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!0}],gettext("Thread has been made hidden."))},n.move=function(){y["default"].show(c["default"].createElement(m["default"],{posts:n.props.posts,thread:n.props.thread}))},n.merge=function(){y["default"].show(c["default"].createElement(f["default"],{thread:n.props.thread}))},n["delete"]=function(){confirm(gettext("Are you sure you want to delete this thread?"))&&(k["default"].dispatch(b.busy()),_["default"]["delete"](n.props.thread.api.index).then(function(e){w["default"].success(gettext("Thread has been deleted.")),window.location=n.props.thread.category.url.index},function(e){k["default"].dispatch(b.release()),w["default"].apiError(e)}))},r=a,l(n,r)}return s(t,e),i(t,[{key:"getPinGloballyButton",value:function(){return 2===this.props.thread.weight?null:this.props.thread.acl.can_pin_globally?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinGlobally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark"),gettext("Pin globally"))):null}},{key:"getPinLocallyButton",value:function(){return 1===this.props.thread.weight?null:this.props.thread.acl.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinLocally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark_border"),gettext("Pin locally"))):null}},{key:"getUnpinButton",value:function(){return 0===this.props.thread.weight?null:this.props.thread.acl.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unpin,type:"button"},c["default"].createElement("span",{className:"material-icon"},"panorama_fish_eye"),gettext("Unpin"))):null}},{key:"getMoveButton",value:function(){return this.props.thread.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.move,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}},{key:"getMergeButton",value:function(){return this.props.thread.acl.can_merge?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.merge,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge"))):null}},{key:"getApproveButton",value:function(){return this.props.thread.is_unapproved&&this.props.thread.acl.can_approve?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.approve,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}},{key:"getOpenButton",value:function(){return this.props.thread.is_closed&&this.props.thread.acl.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.open,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Open"))):null}},{key:"getCloseButton",value:function(){return this.props.thread.is_closed?null:this.props.thread.acl.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.close,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Close"))):null}},{key:"getUnhideButton",value:function(){return this.props.thread.is_hidden&&this.props.thread.acl.can_unhide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unhide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}},{key:"getHideButton",value:function(){return this.props.thread.is_hidden?null:this.props.thread.acl.can_hide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.hide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}},{key:"getDeleteButton",value:function(){return this.props.thread.acl.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this["delete"],type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}},{key:"render",value:function(){return c["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},this.getPinGloballyButton(),this.getPinLocallyButton(),this.getUnpinButton(),this.getMoveButton(),this.getMergeButton(),this.getApproveButton(),this.getOpenButton(),this.getCloseButton(),this.getUnhideButton(),this.getHideButton(),this.getDeleteButton())}}]),t}(c["default"].Component);a["default"]=N},{"../../../../reducers/thread":356,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"./merge":226,"./move":227,react:"react"}],224:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.isModerationVisible=a.ModerationControls=void 0;var r=e("./controls"),o=n(r),l=e("./is-visible"),s=n(l);a.ModerationControls=o["default"],a.isModerationVisible=s["default"]},{"./controls":223,"./is-visible":225}],225:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return!!(e.acl.can_approve&&e.is_unapproved||e.acl.can_close||e.acl.can_delete||e.acl.can_hide||e.acl.can_move||e.acl.can_merge||e.acl.can_pin||e.acl.can_pin_globally&&2!==e.weight||e.acl.can_unhide&&e.is_hidden)}},{}],226:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Merge thread")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../form"),p=r(f),m=e("../../../form-group"),h=r(m),b=e("../../../merge-conflict"),v=r(b),_=e("../../../../reducers/thread"),g=n(_),y=e("../../../../services/ajax"),E=r(y),w=e("../../../../services/modal"),O=r(w),k=e("../../../../services/snackbar"),N=r(k),x=e("../../../../services/store"),j=r(x),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleSuccess=function(e){a.handleSuccessUnmounted(e),a.setState({isLoading:!0})},a.handleSuccessUnmounted=function(e){N["default"].success(gettext("Thread has been merged with other one.")),window.location=e.url},a.handleError=function(e){j["default"].dispatch(g.release()),400===e.status?e.best_answers||e.polls?O["default"].show(d["default"].createElement(v["default"],{api:a.props.thread.api.merge,bestAnswers:e.best_answers,data:{other_thread:a.state.url},polls:e.polls,onError:a.handleError,onSuccess:a.handleSuccessUnmounted})):e.best_answer?N["default"].error(e.best_answer[0]):e.poll?N["default"].error(e.poll[0]):N["default"].error(e.detail):N["default"].apiError(e)},a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(N["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return j["default"].dispatch(g.busy()),E["default"].post(this.props.thread.api.merge,{other_thread:this.state.url})}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(h["default"],{"for":"id_url",label:gettext("Link to thread you want to merge with"),help_text:gettext("Merge will delete current thread and move its contents to the thread specified here.")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading||this.props.thread.isBusy,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading||this.props.thread.isBusy},gettext("Merge thread"))))))}}]),t}(p["default"]);a["default"]=P},{"../../../../reducers/thread":356,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../form":54,"../../../form-group":53,"../../../merge-conflict":57,react:"react"}],227:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return p["default"].createElement("div",{className:"modal-header"},p["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},p["default"].createElement("span",{"aria-hidden":"true"},"×")),p["default"].createElement("h4",{className:"modal-title"},gettext("Move thread")))}function u(e){return p["default"].createElement("div",{className:"modal-dialog",role:"document"},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement(E["default"],null)))}function c(e){return p["default"].createElement("div",{className:"modal-dialog modal-message",role:"document"},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement("div",{className:"message-icon"},p["default"].createElement("span",{className:"material-icon"},"info_outline")),p["default"].createElement("div",{className:"message-body"},p["default"].createElement("p",{className:"lead"},gettext("You can't move this thread at the moment.")),p["default"].createElement("p",null,e.message),p["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok")))))}Object.defineProperty(a,"__esModule",{value:!0});var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i,a.ModalLoading=u,a.ModalMessage=c;var f=e("react"),p=r(f),m=e("../../../form"),h=r(m),b=e("../../../form-group"),v=r(b),_=e("../../../category-select"),g=r(_),y=e("../../../modal-loader"),E=r(y),w=e("../../../../reducers/posts"),O=n(w),k=e("../../../../reducers/thread"),N=n(k),x=e("../../../.."),j=r(x),P=e("../../../../services/ajax"),C=r(P),M=e("../../../../services/modal"),S=r(M),T=e("../../../../services/snackbar"),L=r(T),A=e("../../../../services/store"),R=r(A),I=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){a.changeValue("category",e.target.value)},a.state={isReady:!1,isLoading:!1,isError:!1,category:null,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;C["default"].get(j["default"].get("THREAD_EDITOR_API")).then(function(t){var a=null,n=t.map(function(e){return e.post===!1||a||(a=e.id),Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id})});e.setState({isReady:!0,category:a,categories:n})},function(t){e.setState({isError:t.detail})})}},{key:"send",value:function(){return R["default"].dispatch(N.busy()),C["default"].patch(this.props.thread.api.index,[{op:"replace",path:"category",value:this.state.category}])}},{key:"handleSuccess",value:function(){C["default"].get(this.props.thread.api.posts.index,{page:this.props.posts.page}).then(function(e){R["default"].dispatch(N.replace(e)),R["default"].dispatch(O.load(e.post_set)),R["default"].dispatch(N.release()),L["default"].success(gettext("Thread has been moved.")),S["default"].hide()},function(e){R["default"].dispatch(N.release()),L["default"].apiError(e)})}},{key:"handleError",value:function(e){400===e.status?L["default"].error(e.detail[0]):L["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?p["default"].createElement("div",{className:"modal-dialog",role:"document"},p["default"].createElement("form",{onSubmit:this.handleSubmit},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement("div",{className:"modal-body"},p["default"].createElement(v["default"],{"for":"id_category",label:gettext("New category")},p["default"].createElement(g["default"],{choices:this.state.categories,disabled:this.state.isLoading||this.props.thread.isBusy,id:"id_category",onChange:this.onCategoryChange,value:this.state.category}))),p["default"].createElement("div",{className:"modal-footer"},p["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),p["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading||this.props.thread.isBusy},gettext("Move thread")))))):this.state.isError?p["default"].createElement(c,{message:this.state.isError
-}):p["default"].createElement(u,null)}}]),t}(h["default"]);a["default"]=I},{"../../../..":299,"../../../../reducers/posts":350,"../../../../reducers/thread":356,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../category-select":20,"../../../form":54,"../../../form-group":53,"../../../modal-loader":59,react:"react"}],228:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return d["default"].createElement("div",{className:"row row-paginator"},d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(o,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(l,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(s,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(i,e)))}function o(e){return e.posts.isLoaded&&e.posts.first?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index,title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page"))}function l(e){if(e.posts.isLoaded&&e.posts.page>1){var t="";return e.posts.previous&&(t=e.posts.previous+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+t,title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function s(e){if(e.posts.isLoaded&&e.posts.more){var t="";return e.posts.next&&(t=e.posts.next+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+t,title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function i(e){return e.posts.isLoaded&&e.posts.last?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+e.posts.last+"/",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page"))}function u(e){var t=null;return e.more?(t=ngettext("There is %(more)s more post in this thread.","There are %(more)s more posts in this thread.",e.more),t=interpolate(t,{more:e.more},!0)):t=gettext("There are no more posts in this thread."),d["default"].createElement("p",null,t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return d["default"].createElement("nav",{className:"misago-pagination pull-left"},d["default"].createElement(r,e),d["default"].createElement(u,{more:e.posts.more}))},a.Pager=r,a.FirstPage=o,a.PreviousPage=l,a.NextPage=s,a.LastPage=i,a.More=u;var c=e("react"),d=n(c),f=e("react-router")},{react:"react","react-router":"react-router"}],229:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("button",{className:e.className||"btn btn-primary btn-outline",onClick:e.onClick,type:"button"},o["default"].createElement("span",{className:"material-icon"},"chat"),gettext("Reply"))};var r=e("react"),o=n(r)},{react:"react"}],230:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{participants:e.participants,poll:e.poll,posts:e.posts,thread:e.thread,tick:e.tick.tick,user:e.auth.user}}function o(){var e=c["default"].get("THREAD"),t=e.url.index.replace(e.slug+"-"+e.pk,":slug");return[{path:t,component:(0,l.connect)(r)(i["default"])},{path:t+":page/",component:(0,l.connect)(r)(i["default"])}]}Object.defineProperty(a,"__esModule",{value:!0}),a.select=r,a.paths=o;var l=e("react-redux"),s=e("./route"),i=n(s),u=e("../../index"),c=n(u)},{"../../index":299,"./route":231,"react-redux":"react-redux"}],231:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),c=e("react"),d=r(c),f=e("../participants"),p=r(f),m=e("../poll"),h=e("../posts-list"),b=r(h),v=e("./header"),_=r(v),g=e("./toolbar-top"),y=r(g),E=e("./toolbar-bottom"),w=r(E),O=e("../../reducers/participants"),k=n(O),N=e("../../reducers/poll"),x=n(N),j=e("../../reducers/posts"),P=n(j),C=e("../../reducers/thread"),M=n(C),S=e("../../services/ajax"),T=r(S),L=e("../../services/polls"),A=r(L),R=e("../../services/snackbar"),I=r(R),D=e("../../services/posting"),U=r(D),B=e("../../services/store"),H=r(B),z=e("../../services/page-title"),F=r(z),q=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.update=function(e){H["default"].dispatch(M.replace(e)),H["default"].dispatch(P.load(e.post_set)),e.participants&&H["default"].dispatch(k.replace(e.participants)),e.poll&&H["default"].dispatch(x.replace(e.poll)),n.setPageTitle()},n.openReplyForm=function(){U["default"].open({mode:"REPLY",config:n.props.thread.api.editor,submit:n.props.thread.api.posts.index})},r=a,l(n,r)}return s(t,e),u(t,[{key:"componentDidMount",value:function(){this.shouldFetchData()&&(this.fetchData(),this.setPageTitle()),this.startPollingApi()}},{key:"componentDidUpdate",value:function(){this.shouldFetchData()&&(this.fetchData(),this.startPollingApi(),this.setPageTitle())}},{key:"componentWillUnmount",value:function(){this.stopPollingApi()}},{key:"shouldFetchData",value:function(){if(this.props.posts.isLoaded){var e=1*(this.props.params.page||1);return e!=this.props.posts.page}return!1}},{key:"fetchData",value:function(){var e=this;H["default"].dispatch(P.unload()),T["default"].get(this.props.thread.api.posts.index,{page:this.props.params.page||1},"posts").then(function(t){e.update(t)},function(e){I["default"].apiError(e)})}},{key:"startPollingApi",value:function(){A["default"].start({poll:"thread-posts",url:this.props.thread.api.posts.index,data:{page:this.props.params.page||1},update:this.update,frequency:12e4,delayed:!0})}},{key:"stopPollingApi",value:function(){A["default"].stop("thread-posts")}},{key:"setPageTitle",value:function(){F["default"].set({title:this.props.thread.title,parent:this.props.thread.category.name,page:1*(this.props.params.page||1)})}},{key:"render",value:function(){var e="page page-thread";return this.props.thread.category.css_class&&(e+=" page-thread-"+this.props.thread.category.css_class),d["default"].createElement("div",{className:e},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement(_["default"],this.props)),d["default"].createElement("div",{className:"container"},d["default"].createElement(y["default"],i({openReplyForm:this.openReplyForm},this.props)),d["default"].createElement(m.Poll,{poll:this.props.poll,thread:this.props.thread,user:this.props.user}),d["default"].createElement(p["default"],{participants:this.props.participants,thread:this.props.thread,user:this.props.user}),d["default"].createElement(b["default"],this.props),d["default"].createElement(w["default"],i({openReplyForm:this.openReplyForm},this.props))))}}]),t}(d["default"].Component);a["default"]=q},{"../../reducers/participants":347,"../../reducers/poll":348,"../../reducers/posts":350,"../../reducers/thread":356,"../../services/ajax":361,"../../services/page-title":369,"../../services/polls":370,"../../services/posting":371,"../../services/snackbar":372,"../../services/store":373,"../participants":98,"../poll":103,"../posts-list":148,"./header":215,"./toolbar-bottom":233,"./toolbar-top":234,react:"react"}],232:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e===!0?"star":e===!1?"star_half":"star_border"}function u(e){return e===!0?gettext("E-mail"):e===!1?gettext("Enabled"):gettext("Disabled")}function c(e){return m["default"].createElement("ul",{className:e.dropdownClassName||"dropdown-menu stick-to-bottom"},m["default"].createElement(O,e),m["default"].createElement(k,e),m["default"].createElement(N,e))}function d(e,t,a){var n={subscription:e.subscription};w["default"].dispatch(b.update({subscription:t})),_["default"].patch(e.api.index,[{op:"replace",path:"subscription",value:a}]).then(function(e){w["default"].dispatch(b.update(e))},function(e){400===e.status?y["default"].error(e.detail[0]):y["default"].apiError(e),w["default"].dispatch(b.update(n))})}Object.defineProperty(a,"__esModule",{value:!0}),a.Email=a.Enable=a.Disable=void 0;var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return e.user.id?m["default"].createElement("div",{className:e.className},m["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",type:"button"},m["default"].createElement("span",{className:"material-icon"},i(e.thread.subscription)),u(e.thread.subscription)),m["default"].createElement(c,e)):null},a.getIcon=i,a.getLabel=u,a.Dropdown=c,a.update=d;var p=e("react"),m=r(p),h=e("../../reducers/thread"),b=n(h),v=e("../../services/ajax"),_=r(v),g=e("../../services/snackbar"),y=r(g),E=e("../../services/store"),w=r(E),O=a.Disable=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){null!==n.props.thread.subscription&&d(n.props.thread,null,"unsubscribe")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star_border"),gettext("Unsubscribe")))}}]),t}(m["default"].Component),k=a.Enable=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.thread.subscription!==!1&&d(n.props.thread,!1,"notify")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star_half"),gettext("Subscribe")))}}]),t}(m["default"].Component),N=a.Email=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.thread.subscription!==!0&&d(n.props.thread,!0,"email")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star"),gettext("Subscribe with e-mail")))}}]),t}(m["default"].Component)},{"../../reducers/thread":356,"../../services/ajax":361,"../../services/snackbar":372,"../../services/store":373,react:"react"}],233:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.visible?d["default"].createElement("div",{className:"col-md-5"},e.children):null}function o(e){return e.user.id?d["default"].createElement("div",{className:"col-sm-4 hidden-xs"},d["default"].createElement(m["default"],e)):null}function l(e){var t="col-xs-6";return e.thread.acl.can_reply||(t="col-xs-12"),d["default"].createElement("div",{className:t+" col-sm-4"},d["default"].createElement(_["default"],u({btnClassName:"btn-block",className:"dropup"},e)))}function s(e){return e.thread.acl.can_reply?d["default"].createElement("div",{className:"col-xs-6 col-sm-4"},d["default"].createElement(b["default"],{className:"btn btn-primary btn-block btn-outline",onClick:e.onClick})):null}function i(e){return e.thread.acl.can_reply?null:d["default"].createElement("div",{className:"hidden-xs hidden-sm col-sm-4"})}Object.defineProperty(a,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return d["default"].createElement("div",{className:"row row-toolbar"},d["default"].createElement("div",{className:"col-xs-12 text-center visible-xs-block"},d["default"].createElement(f.More,{more:e.posts.more}),d["default"].createElement("div",{className:"toolbar-vertical-spacer"})),d["default"].createElement("div",{className:"col-md-7"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-4 col-md-5"},d["default"].createElement(f.Pager,e)),d["default"].createElement("div",{className:"col-sm-8 col-md-7 hidden-xs"},d["default"].createElement(f.More,{more:e.posts.more})))),d["default"].createElement(r,{visible:!!e.user.id},d["default"].createElement("div",{className:"toolbar-vertical-spacer hidden-md hidden-lg"}),d["default"].createElement("div",{className:"row"},d["default"].createElement(i,e),d["default"].createElement(o,e),d["default"].createElement(l,e),d["default"].createElement(s,{thread:e.thread,onClick:e.openReplyForm}))))},a.Options=r,a.Moderation=o,a.Subscription=l,a.Reply=s,a.Spacer=i;var c=e("react"),d=n(c),f=e("./paginator"),p=e("./moderation/posts"),m=n(p),h=e("./reply-button"),b=n(h),v=e("./subscription"),_=n(v)},{"./moderation/posts":220,"./paginator":228,"./reply-button":229,"./subscription":232,react:"react"}],234:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.user,a="col-xs-3 col-sm-3 col-md-5";return t.is_anonymous&&(a="col-xs-12 col-sm-3 col-md-5"),w["default"].createElement("div",{className:a},w["default"].createElement("div",{className:"row hidden-xs hidden-sm"},w["default"].createElement(d,{thread:e.thread}),w["default"].createElement(i,{thread:e.thread}),w["default"].createElement(u,{thread:e.thread}),w["default"].createElement(c,{thread:e.thread})),w["default"].createElement(f,e))}function i(e){return e.thread.is_new?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.new_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to first new post")},gettext("New"))):null}function u(e){return e.thread.best_answer?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.best_answer,className:"btn btn-default btn-block btn-outline",title:gettext("Go to best answer")},gettext("Best answer"))):null}function c(e){return e.thread.has_unapproved_posts&&e.thread.acl.can_approve?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.unapproved_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to first unapproved post")},gettext("Unapproved"))):null}function d(e){return w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to last post")},gettext("Last")))}function f(e){var t=e.user;return t.is_anonymous?w["default"].createElement("div",{className:"visible-xs-block visible-sm-block"},w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-default btn-block btn-outline"},gettext("Last post"))):w["default"].createElement("div",{className:"dropdown visible-xs-block visible-sm-block"},w["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",type:"button"},w["default"].createElement("span",{className:"material-icon"},"expand_more"),w["default"].createElement("span",{className:"btn-text hidden-xs"},gettext("Options"))),w["default"].createElement("ul",{className:"dropdown-menu"},w["default"].createElement(M,e),w["default"].createElement(p,e),w["default"].createElement(m,e),w["default"].createElement(h,e)))}function p(e){return e.thread.is_new?w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.new_post,className:"btn btn-link"},gettext("Go to first new post"))):null}function m(e){return e.thread.has_unapproved_posts&&e.thread.acl.can_approve?w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.unapproved_post,className:"btn btn-link"},gettext("Go to first unapproved post"))):null}function h(e){return w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-link"},gettext("Go to last post")))}function b(e){return e.thread.acl.can_reply?w["default"].createElement("div",{className:"col-sm-4 hidden-xs"},w["default"].createElement(k["default"],{className:"btn btn-primary btn-block btn-outline",onClick:e.openReplyForm})):null}function v(e){return e.user.id?w["default"].createElement("div",{className:"col-xs-12 col-sm-4"},w["default"].createElement(x["default"],y({className:"dropdown",dropdownClassName:"dropdown-menu dropdown-menu-right stick-to-bottom"},e))):null}function _(e){return e.visible?w["default"].createElement("div",{className:"col-sm-4 hidden-xs"}):null}Object.defineProperty(a,"__esModule",{value:!0}),a.StartPollCompact=a.StartPoll=void 0;var g=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),y=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){var t=!e.thread.acl.can_start_poll||e.thread.poll;return w["default"].createElement("div",{className:"row row-toolbar row-toolbar-bottom-margin"},w["default"].createElement(s,e),w["default"].createElement("div",{className:"col-xs-9 col-md-5 col-md-offset-2"},w["default"].createElement("div",{className:"row"},w["default"].createElement(_,{visible:!e.user.id}),w["default"].createElement(_,{visible:t}),w["default"].createElement(v,e),w["default"].createElement(C,e),w["default"].createElement(b,e))))},a.GotoMenu=s,a.GotoNew=i,a.GotoBestAnswer=u,a.GotoUnapproved=c,a.GotoLast=d,a.CompactOptions=f,a.GotoNewCompact=p,a.GotoUnapprovedCompact=m,a.GotoLastCompact=h,a.Reply=b,a.SubscriptionMenu=v,a.Spacer=_;var E=e("react"),w=n(E),O=e("./reply-button"),k=n(O),N=e("./subscription"),x=n(N),j=e("../../services/posting"),P=n(j),C=a.StartPoll=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){P["default"].open({mode:"POLL",submit:n.props.thread.api.poll,thread:n.props.thread,poll:null})},l=a,o(n,l)}return l(t,e),g(t,[{key:"render",value:function(){return!this.props.thread.acl.can_start_poll||this.props.thread.poll?null:w["default"].createElement("div",{className:"col-sm-4 hidden-xs"},w["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",onClick:this.onClick,type:"button"},w["default"].createElement("span",{className:"material-icon"},"poll"),gettext("Add poll")))}}]),t}(w["default"].Component),M=a.StartPollCompact=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),g(t,[{key:"render",value:function(){return!this.props.thread.acl.can_start_poll||this.props.thread.poll?null:w["default"].createElement("li",null,w["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},gettext("Add poll")))}}]),t}(C)},{"../../services/posting":371,"./reply-button":229,"./subscription":232,react:"react"}],235:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.isLoaded?0===e.threads.length?o["default"].createElement(s["default"],{diffSize:e.diffSize,applyDiff:e.applyDiff},e.children):o["default"].createElement(u["default"],{activeCategory:e.category,categories:e.categories,list:e.list,threads:e.threads,diffSize:e.diffSize,applyDiff:e.applyDiff,showOptions:e.showOptions,selection:e.selection,busyThreads:e.busyThreads}):o["default"].createElement(d["default"],null)};var r=e("react"),o=n(r),l=e("./list/empty"),s=n(l),i=e("./list/ready"),u=n(i),c=e("./list/preview"),d=n(c)},{"./list/empty":237,"./list/preview":238,"./list/ready":239,react:"react"}],236:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=ngettext("There is %(threads)s new or updated thread. Click this message to show it.","There are %(threads)s new or updated threads. Click this message to show them.",e);return interpolate(t,{threads:e},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.diffSize,a=e.applyDiff;return 0===t?null:l["default"].createElement("li",{className:"list-group-item threads-diff-message"},l["default"].createElement("button",{type:"button",className:"btn btn-block btn-default",onClick:a},l["default"].createElement("span",{className:"material-icon"},"cached"),l["default"].createElement("span",{className:"diff-message"},r(t))))},a.getMessage=r;var o=e("react"),l=n(o)},{react:"react"}],237:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./diff-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getDiffMessage",value:function(){return 0===this.props.diffSize?null:u["default"].createElement(d["default"],{applyDiff:this.props.applyDiff,diffSize:this.props.diffSize})}},{key:"render",value:function(){return u["default"].createElement("div",{className:"threads-list ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.getDiffMessage(),this.props.children))}}]),t}(u["default"].Component);a["default"]=f},{"./diff-message":236,react:"react"}],238:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../thread/preview"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",{className:"threads-list ui-preview"},u["default"].createElement("ul",{className:"list-group"},u["default"].createElement(d["default"],null)))}}]),t}(u["default"].Component);a["default"]=f},{"../thread/preview":246,react:"react"}],239:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"threads-list ui-ready"},o["default"].createElement("ul",{className:"list-group"},o["default"].createElement(s["default"],{diffSize:e.diffSize,applyDiff:e.applyDiff}),e.threads.map(function(t){return o["default"].createElement(u["default"],{activeCategory:e.activeCategory,categories:e.categories,list:e.list,thread:t,showOptions:e.showOptions,isSelected:e.selection.indexOf(t.id)>=0,isBusy:e.busyThreads.indexOf(t.id)>=0,key:t.id})})))};var r=e("react"),o=n(r),l=e("./diff-message"),s=n(l),i=e("../thread/ready"),u=n(i)},{"../thread/ready":247,"./diff-message":236,react:"react"}],240:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-hidden"},f["default"].createElement("span",{className:"material-icon"},"visibility_off"),f["default"].createElement("span",{className:a},gettext("Hidden"))):null}function o(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-closed"},f["default"].createElement("span",{className:"material-icon"},"lock_outline"),f["default"].createElement("span",{className:a},gettext("Closed"))):null}function l(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-poll"},f["default"].createElement("span",{className:"material-icon"},"assessment"),f["default"].createElement("span",{className:a},gettext("Poll"))):null}function s(e){var t=e.thread;return t.best_answer?f["default"].createElement("a",{className:"visible-xs-inline-block thread-detail-answered",href:t.url.best_answer},f["default"].createElement("span",{className:"material-icon"},"check_box")):null}function i(e){var t=e.replies,a=e.forceFullText,n=ngettext("%(replies)s reply","%(replies)s replies",t),r="",o="";return a?(r="detail-text hide",o="detail-text"):(r="detail-text visible-xs-inline-block",o="detail-text hidden-xs"),f["default"].createElement("span",{className:"thread-detail-replies"},f["default"].createElement("span",{className:"material-icon"},"forum"),f["default"].createElement("span",{className:r},t),f["default"].createElement("span",{className:o},interpolate(n,{replies:t},!0)))}function u(e){var t=e.datetime,a=e.url;return f["default"].createElement("a",{className:"visible-sm-inline-block thread-detail-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}function c(e){var t=e.posterName,a=e.url,n="visible-sm-inline-block item-title thread-last-poster";return a?f["default"].createElement("a",{className:n,href:a},t):f["default"].createElement("span",{className:n},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isBusy,n=e.showOptions,d=e.isSelected,p=e.thread,b="col-xs-12 col-sm-12";n&&(b=p.moderation.length?"col-xs-6 col-sm-12":"col-xs-9 col-sm-12");var v=0;p.is_hidden&&(v+=1),p.is_closed&&(v+=1),p.has_poll&&(v+=1);var _=n&&3===v,g="detail-text hidden-xs";return _&&(g+=" hidden-sm"),f["default"].createElement("div",{className:"row thread-details-bottom"},f["default"].createElement("div",{className:b},f["default"].createElement(m["default"],{className:"item-title thread-detail-category hidden-xs",category:t}),f["default"].createElement(r,{textClassName:g,display:p.is_hidden}),f["default"].createElement(o,{textClassName:g,display:p.is_closed}),f["default"].createElement(l,{textClassName:g,
-display:p.has_poll}),f["default"].createElement(s,{thread:p}),f["default"].createElement(i,{forceFullText:!n||v<2,replies:p.replies}),f["default"].createElement(u,{datetime:p.last_post_on,url:p.url.last_post}),f["default"].createElement(c,{posterName:p.last_poster_name,url:p.url.last_poster})),f["default"].createElement(h.OptionsXs,{disabled:a,display:n,isSelected:d,thread:p}))},a.HiddenLabel=r,a.ClosedLabel=o,a.PollLabel=l,a.BestAnswerLabel=s,a.RepliesLabel=i,a.LastReplyLabel=u,a.LastPoster=c;var d=e("react"),f=n(d),p=e("./category"),m=n(p),h=e("../options")},{"../options":245,"./category":241,react:"react"}],241:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.className;return t?(t.css_class&&(a+=" thread-detail-category-"+t.css_class),o["default"].createElement("a",{className:a,href:t.url.index},t.name)):null};var r=e("react"),o=n(r)},{react:"react"}],242:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.TopDetails=a.BottomDetails=void 0;var r=e("./bottom"),o=n(r),l=e("./top"),s=n(l);a.BottomDetails=o["default"],a.TopDetails=s["default"]},{"./bottom":240,"./top":243}],243:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.isRead,a=e.url;return t?null:d["default"].createElement("a",{className:"thread-detail-new",href:a},d["default"].createElement("span",{className:"material-icon"},"comment"),d["default"].createElement("span",{className:"detail-text"},gettext("New posts")))}function o(e){var t=e.weight;if(0===t)return null;var a="thread-detail-pinned-globally",n="bookmark",r=gettext("Pinned globally");return 1===t&&(a="thread-detail-pinned-locally",n="bookmark_border",r=gettext("Pinned locally")),d["default"].createElement("span",{className:a},d["default"].createElement("span",{className:"material-icon"},n),d["default"].createElement("span",{className:"detail-text"},r))}function l(e){var t=e.posts,a=e.thread;if(!t&&!a)return null;var n="thread-detail-unapproved-posts",r="remove_circle_outline",o=gettext("Unapproved posts");return a&&(n="thread-detail-unapproved",r="remove_circle",o=gettext("Unapproved")),d["default"].createElement("span",{className:n},d["default"].createElement("span",{className:"material-icon"},r),d["default"].createElement("span",{className:"detail-text"},o))}function s(e){var t=e.thread;return t.best_answer?d["default"].createElement("a",{className:"hidden-xs thread-detail-answered",href:t.url.best_answer},d["default"].createElement("span",{className:"material-icon"},"check_box"),d["default"].createElement("span",{className:"detail-text"},gettext("Answered"))):null}function i(e){var t=e.datetime,a=e.url;return d["default"].createElement("a",{className:"visible-xs-inline-block thread-detail-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}function u(e){var t=e.posterName,a=e.url;return a?d["default"].createElement("a",{className:"visible-xs-inline-block item-title thread-last-poster",href:a},t):d["default"].createElement("span",{className:"visible-xs-inline-block item-title thread-last-poster"},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.thread;return d["default"].createElement("div",{className:"thread-details-top"},d["default"].createElement(r,{isRead:a.is_read,url:a.url.new_post}),d["default"].createElement(o,{weight:a.weight}),d["default"].createElement(l,{thread:a.is_unapproved,posts:a.has_unapproved_posts}),d["default"].createElement(s,{thread:a}),d["default"].createElement(p["default"],{className:"item-title thread-detail-category visible-xs-inline-block",category:t}),d["default"].createElement(i,{datetime:a.last_post_on,url:a.url.last_post}),d["default"].createElement(u,{posterName:a.last_poster_name,url:a.url.last_poster}))},a.NewLabel=r,a.PinnedLabel=o,a.UnapprovedLabel=l,a.BestAnswerLabel=s,a.LastReplyLabel=i,a.LastPoster=u;var c=e("react"),d=n(c),f=e("./category"),p=n(f)},{"./category":241,react:"react"}],244:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.datetime,a=e.url;return l["default"].createElement("a",{className:"thread-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.thread;return l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left"},l["default"].createElement(c["default"],{className:"thread-last-poster-avatar",title:t.last_poster_name,url:t.url.last_poster},l["default"].createElement(i["default"],{className:"media-object",size:40,user:t.last_poster}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement(c["default"],{className:"item-title thread-last-poster",url:t.url.last_poster},t.last_poster_name),l["default"].createElement(r,{datetime:t.last_post_on,url:t.url.last_post})))},a.Timestamp=r;var o=e("react"),l=n(o),s=e("../../avatar"),i=n(s),u=e("./user-url"),c=n(u)},{"../../avatar":5,"./user-url":252,react:"react"}],245:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){var t=e.display,a=e.disabled,n=e.isSelected,r=e.thread;if(!t)return null;var o="col-sm-2 col-md-2 hidden-xs";return r.moderation.length&&(o="col-sm-3 col-md-2 hidden-xs"),f["default"].createElement("div",{className:o},f["default"].createElement("div",{className:"row thread-options"},f["default"].createElement(b["default"],{thread:r,disabled:a}),f["default"].createElement(m["default"],{thread:r,disabled:a}),f["default"].createElement(E,{thread:r,disabled:a,isSelected:n})))}function u(e){var t=e.display,a=e.disabled,n=e.isSelected,r=e.thread;if(!t)return null;var o="";return o+=r.moderation.length?"col-xs-6":"col-xs-3",o+=" visible-xs-block thread-options-xs",f["default"].createElement("div",{className:o},f["default"].createElement("div",{className:"row thread-options"},f["default"].createElement(b["default"],{thread:r,disabled:a}),f["default"].createElement(m["default"],{thread:r,disabled:a}),f["default"].createElement(E,{thread:r,disabled:a,isSelected:n})))}Object.defineProperty(a,"__esModule",{value:!0}),a.Checkbox=void 0;var c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Options=i,a.OptionsXs=u;var d=e("react"),f=r(d),p=e("./subscription/compact"),m=r(p),h=e("./subscription/full"),b=r(h),v=e("../../../reducers/selection"),_=n(v),g=e("../../../services/store"),y=r(g),E=a.Checkbox=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.toggleSelection=function(){y["default"].dispatch(_.item(n.props.thread.id))},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.disabled,a=e.isSelected,n=e.thread;return n.moderation.length?f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-default btn-icon btn-block",onClick:this.toggleSelection,disabled:t},f["default"].createElement("span",{className:"material-icon"},a?"check_box":"check_box_outline_blank"))):null}}]),t}(f["default"].Component)},{"../../../reducers/selection":354,"../../../services/store":373,"./subscription/compact":248,"./subscription/full":249,react:"react"}],246:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../../utils/random"),f=n(d),p=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("li",{className:"list-group-item thread-preview"},c["default"].createElement("div",{className:"thread-details-top visible-xs-block"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," ")),c["default"].createElement("span",{className:"item-title thread-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](60,200)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text hidden-xs",style:{width:f["int"](60,200)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text hidden-xs",style:{width:f["int"](60,200)+"px"}}," ")),c["default"].createElement("div",{className:"thread-details-bottom"},c["default"].createElement("div",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "))))}}]),t}(c["default"].Component);a["default"]=p},{"../../../utils/random":384,react:"react"}],247:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a,n){var r=["list-group-item"];return n&&n.css_class&&(r.push("list-group-category-has-flavor"),r.push("list-group-item-category-"+n.css_class)),e?r.push("thread-read"):r.push("thread-new"),t?r.push("thread-busy"):a&&r.push("thread-selected"),r.join(" ")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.activeCategory,a=e.categories,n=(e.list,e.thread),o=e.isBusy,s=e.isSelected,c=e.showOptions,p=null;t.id!==n.category&&(p=a[n.category]);var h=p||t,b="thread-main col-xs-12";return b+=c?n.moderation.length?" col-sm-9 col-md-7":" col-sm-10 col-md-7":" col-sm-12 col-md-9",l["default"].createElement("li",{className:r(n.is_read,o,s,h)},l["default"].createElement(u.TopDetails,{category:p,thread:n}),l["default"].createElement("div",{className:"row thread-row"},l["default"].createElement("div",{className:b},l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left hidden-xs"},l["default"].createElement(m["default"],{className:"thread-starter-avatar",title:n.starter_name,url:n.url.starter},l["default"].createElement(i["default"],{size:40,user:n.starter}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement("a",{href:n.url.index,className:"item-title thread-title"},n.title),l["default"].createElement(u.BottomDetails,{category:p,disabled:o,isSelected:s,showOptions:c,thread:n})))),l["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm thread-last-action"},l["default"].createElement(d["default"],{thread:n})),l["default"].createElement(f.Options,{disabled:o,display:c,isSelected:s,thread:n})))},a.getClassName=r;var o=e("react"),l=n(o),s=e("../../avatar"),i=n(s),u=e("./details"),c=e("./last-action"),d=n(c),f=e("./options"),p=e("./user-url"),m=n(p)},{"../../avatar":5,"./details":242,"./last-action":244,"./options":245,"./user-url":252,react:"react"}],248:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./full"),d=n(c),f=e("./modal"),p=n(f),m=e("../../../../services/modal"),h=n(m),b=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showOptions=function(){h["default"].show(u["default"].createElement(p["default"],{thread:n.props.thread}))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.thread.moderation,t="";return t+=e.length?"col-xs-6":"col-xs-12",t+=" hidden-md hidden-lg",u["default"].createElement("div",{className:t},u["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,onClick:this.showOptions},u["default"].createElement("span",{className:"material-icon"},this.getIcon())))}}]),t}(d["default"]);a["default"]=b},{"../../../../services/modal":367,"./full":249,"./modal":250,react:"react"}],249:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.moderation,a=e.subscription;if(t.length)return null;var n=gettext("Disabled");return a===!0?n=gettext("E-mail"):a===!1&&(n=gettext("Enabled")),c["default"].createElement("span",{className:"btn-text"},n)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Label=s;var u=e("react"),c=n(u),d=e("./options"),f=n(d),p=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getIcon",value:function(){return this.props.thread.subscription===!0?"star":this.props.thread.subscription===!1?"star_half":"star_border"}},{key:"getClassName",value:function(){return this.props.thread.subscription===!0?"btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-full dropdown-toggle":this.props.thread.subscription===!1?"btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-half dropdown-toggle":"btn btn-default btn-icon btn-block btn-subscribe dropdown-toggle"}},{key:"render",value:function(){var e=this.props.thread,t=e.moderation,a=e.subscription,n=!t.length,r=n?"col-xs-12":"col-xs-6";return r+=" hidden-xs hidden-sm",c["default"].createElement("div",{className:r},c["default"].createElement("div",{className:"btn-group btn-group-justified"},c["default"].createElement("div",{className:"btn-group"},c["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},c["default"].createElement("span",{className:"material-icon"},this.getIcon()),c["default"].createElement(s,{moderation:t,subscription:a})),c["default"].createElement(f["default"],{className:"dropdown-menu dropdown-menu-right",thread:this.props.thread}))))}}]),t}(c["default"].Component);a["default"]=p},{"./options":251,react:"react"}],250:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./options"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-dialog modal-sm",role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Change subscription"))),u["default"].createElement(d["default"],{className:"modal-menu",thread:this.props.thread})))}}]),t}(u["default"].Component);a["default"]=f},{"./options":251,react:"react"}],251:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../../button"),d=(n(c),e("../../../../reducers/threads")),f=e("../../../../services/ajax"),p=n(f),m=e("../../../../services/modal"),h=n(m),b=e("../../../../services/snackbar"),v=n(b),_=e("../../../../services/store"),g=n(_),y={unsubscribe:null,notify:!1,email:!0},E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.setSubscription=function(e){h["default"].hide(),a.setState({isLoading:!0});var t=a.props.thread.subscription;g["default"].dispatch((0,d.patch)(a.props.thread,{subscription:y[e]})),p["default"].patch(a.props.thread.api.index,[{op:"replace",path:"subscription",value:e}]).then(function(){a.setState({isLoading:!1})},function(e){a.setState({isLoading:!1}),g["default"].dispatch((0,d.patch)(a.props.thread,{subscription:y[t]})),v["default"].apiError(e)})},a.unsubscribe=function(){a.setSubscription("unsubscribe")},a.notify=function(){a.setSubscription("notify")},a.email=function(){a.setSubscription("email")},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("ul",{className:this.props.className},u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.unsubscribe},u["default"].createElement("span",{className:"material-icon"},"star_border"),gettext("Unsubscribe"))),u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.notify},u["default"].createElement("span",{className:"material-icon"},"star_half"),gettext("Subscribe"))),u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.email},u["default"].createElement("span",{className:"material-icon"},"star"),gettext("Subscribe with e-mail"))))}}]),t}(u["default"].Component);a["default"]=E},{"../../../../reducers/threads":357,"../../../../services/ajax":361,"../../../../services/modal":367,"../../../../services/snackbar":372,"../../../../services/store":373,"../../../button":7,react:"react"}],252:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.children,a=e.className,n=e.title,r=e.url;return r?o["default"].createElement("a",{className:a,href:r,title:n},t):o["default"].createElement("span",{className:a,title:n},t)};var r=e("react"),o=n(r)},{react:"react"}],253:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Subcategory=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-router"),d=a.Subcategory=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getUrl",value:function(){return this.props.listPath?this.props.category.url.index+this.props.listPath:this.props.category.url.index}},{key:"render",value:function(){return u["default"].createElement("li",null,u["default"].createElement(c.Link,{to:this.getUrl(),className:"btn btn-link"},this.props.category.name))}}]),t}(u["default"].Component),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){var e=this;return u["default"].createElement("div",{className:"dropdown category-picker"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline dropdown-toggle btn-block","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},u["default"].createElement("span",{className:"material-icon"},"label_outline"),u["default"].createElement("span",{className:"hidden-xs"},gettext("Category"))),u["default"].createElement("ul",{className:"dropdown-menu stick-to-bottom categories-menu"},this.props.choices.map(function(t){return e.props.categories[t]?u["default"].createElement(d,{category:e.props.categories[t],listPath:e.props.list.path,key:t}):null})))}}]),t}(u["default"].Component);a["default"]=f},{react:"react","react-router":"react-router"}],254:[function(e,t,a){"use strict";function n(e,t){return e.last_post>t.last_post?-1:e.last_post<t.last_post?1:0}function r(e,t){return 2===e.weight&&e.weight>t.weight?-1:2===t.weight&&e.weight<t.weight?1:n(e,t)}function o(e,t){return e.weight>t.weight?-1:e.weight<t.weight?1:n(e,t)}Object.defineProperty(a,"__esModule",{value:!0}),a.compareLastPostAge=n,a.compareGlobalWeight=r,a.compareWeight=o},{}],255:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../page-lead"),d=n(c),f=e("./toolbar"),p=n(f),m=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getCategoryDescription",value:function(){return this.props.pageLead?u["default"].createElement("div",{className:"category-description"},u["default"].createElement("div",{className:"page-lead"},u["default"].createElement("p",null,this.props.pageLead))):this.props.route.category.description?u["default"].createElement("div",{className:"category-description"},u["default"].createElement(d["default"],{copy:this.props.route.category.description.html})):null}},{key:"getDisableToolbar",value:function(){return!this.props.isLoaded||this.props.isBusy||this.props.busyThreads.length}},{key:"getToolbar",value:function(){var e=this.props.subcategories.length||this.props.user.id;return e?u["default"].createElement(p["default"],{subcategories:this.props.subcategories,categories:this.props.route.categories,categoriesMap:this.props.route.categoriesMap,list:this.props.route.list,threads:this.props.threads,moderation:this.props.moderation,selection:this.props.selection,selectAllThreads:this.props.selectAllThreads,selectNoneThreads:this.props.selectNoneThreads,addThreads:this.props.addThreads,freezeThread:this.props.freezeThread,deleteThread:this.props.deleteThread,updateThread:this.props.updateThread,api:this.props.api,route:this.props.route,disabled:this.getDisableToolbar(),user:this.props.user}):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"container"},this.getCategoryDescription(),this.getToolbar(),this.props.children)}}]),t}(u["default"].Component);a["default"]=m},{"../page-lead":89,"./toolbar":266,react:"react"}],256:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.categories,a=e.category;if(!a)return null;var n=t[a];return c["default"].createElement(d.Link,{className:"go-back-sm visible-xs-block",to:n.url.index},c["default"].createElement("span",{className:"material-icon"},"chevron_left"),n.parent?n.name:gettext("Threads"))}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ParentCategory=s;var u=e("react"),c=n(u),d=e("react-router"),f=e("../button"),p=n(f),m=e("../dropdown-toggle"),h=(n(m),e("./nav")),b=n(h),v=e("../../services/ajax"),_=(n(v),e("../../services/posting")),g=n(_),y=e("../../services/snackbar"),E=(n(y),e("../../services/store")),w=(n(E),e("../..")),O=n(w),k=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.startThread=function(){g["default"].open(a.props.startThread||{mode:"START",config:O["default"].get("THREAD_EDITOR_API"),submit:O["default"].get("THREADS_API"),category:a.props.route.category.id})},a.state={isBusy:!1},a}return l(t,e),i(t,[{key:"hasGoBackButton",value:function(){return!!this.props.route.category.parent}},{key:"getGoBackButton",value:function(){if(!this.props.route.category.parent)return null;var e=this.props.categories[this.props.route.category.parent];return c["default"].createElement("div",{className:"hidden-xs col-sm-2 col-lg-1"},c["default"].createElement(d.Link,{className:"btn btn-default btn-icon btn-aligned btn-go-back btn-block btn-outline",to:e.url.index+this.props.route.list.path},c["default"].createElement("span",{className:"material-icon"},"keyboard_arrow_left")))}},{key:"getStartThreadButton",value:function(){return this.props.user.id?c["default"].createElement(p["default"],{className:"btn-primary btn-block btn-outline",onClick:this.startThread,disabled:this.props.disabled},c["default"].createElement("span",{className:"material-icon"},"chat"),gettext("Start thread")):null}},{key:"render",value:function(){var e="col-xs-12";this.hasGoBackButton()&&(e+=" col-sm-10 col-lg-11 sm-align-row-buttons");var t=!!this.props.user.id;return c["default"].createElement("div",{className:"page-header-bg"},c["default"].createElement("div",{className:"page-header"},c["default"].createElement("div",{className:"container"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:t?"col-sm-9 col-md-10":"col-xs-12"},c["default"].createElement("div",{
-className:"row"},this.getGoBackButton(),c["default"].createElement("div",{className:e},c["default"].createElement(s,{categories:this.props.categories,category:this.props.route.category.parent}),c["default"].createElement("h1",null,this.props.title)))),t&&c["default"].createElement("div",{className:"col-sm-3 col-md-2 xs-margin-top"},this.getStartThreadButton()))),c["default"].createElement(b["default"],{baseUrl:this.props.route.category.url.index,list:this.props.route.list,lists:this.props.route.lists})))}}]),t}(c["default"].Component);a["default"]=k},{"../..":299,"../../services/ajax":361,"../../services/posting":371,"../../services/snackbar":372,"../../services/store":373,"../button":7,"../dropdown-toggle":26,"./nav":263,react:"react","react-router":"react-router"}],257:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return"all"===this.props.list.type?this.props.emptyMessage?u["default"].createElement("li",{className:"list-group-item empty-message"},u["default"].createElement("p",{className:"lead"},this.props.emptyMessage),u["default"].createElement("p",null,gettext("Why not start one yourself?"))):u["default"].createElement("li",{className:"list-group-item empty-message"},u["default"].createElement("p",{className:"lead"},this.props.category.special_role?gettext("There are no threads on this forum... yet!"):gettext("There are no threads in this category.")),u["default"].createElement("p",null,gettext("Why not start one yourself?"))):u["default"].createElement("li",{className:"list-group-item empty-message"},gettext("No threads matching specified criteria were found."))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],258:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./errors-list"),f=r(d),p=e("./merge"),m=r(p),h=e("./move"),b=r(h),v=e("../../../reducers/selection"),_=(n(v),e("../../../services/ajax")),g=r(_),y=e("../../../services/modal"),E=r(y),w=e("../../../services/snackbar"),O=r(w),k=e("../../../services/store"),N=(r(k),e("../../../utils/countdown")),x=(r(N),function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.callApi=function(e,t){var a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;n.props.threads.forEach(function(e){n.props.freezeThread(e.id)});var r=n.props.threads.map(function(e){return e.id});e.push({op:"add",path:"acl",value:!0}),g["default"].patch(n.props.api,{ids:r,ops:e}).then(function(e){n.props.threads.forEach(function(e){n.props.freezeThread(e.id)}),e.forEach(function(e){n.props.updateThread(e)}),O["default"].success(t),a&&a()},function(e){if(n.props.threads.forEach(function(e){n.props.freezeThread(e.id)}),400!==e.status)return O["default"].apiError(e);var t=[],a={};n.props.threads.forEach(function(e){a[e.id]=e}),e.forEach(function(e){var n=e.id,r=e.detail;"undefined"!=typeof a[n]&&t.push({errors:r,thread:a[n]})}),E["default"].show(c["default"].createElement(f["default"],{errors:t}))})},n.pinGlobally=function(){n.callApi([{op:"replace",path:"weight",value:2}],gettext("Selected threads were pinned globally."))},n.pinLocally=function(){n.callApi([{op:"replace",path:"weight",value:1}],gettext("Selected threads were pinned locally."))},n.unpin=function(){n.callApi([{op:"replace",path:"weight",value:0}],gettext("Selected threads were unpinned."))},n.approve=function(){n.callApi([{op:"replace",path:"is-unapproved",value:!1}],gettext("Selected threads were approved."))},n.open=function(){n.callApi([{op:"replace",path:"is-closed",value:!1}],gettext("Selected threads were opened."))},n.close=function(){n.callApi([{op:"replace",path:"is-closed",value:!0}],gettext("Selected threads were closed."))},n.unhide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!1}],gettext("Selected threads were unhidden."))},n.hide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!0}],gettext("Selected threads were hidden."))},n.move=function(){E["default"].show(c["default"].createElement(b["default"],{callApi:n.callApi,categories:n.props.categories,categoriesMap:n.props.categoriesMap,route:n.props.route,user:n.props.user}))},n.merge=function(){var e=[];if(n.props.threads.forEach(function(t){t.acl.can_merge||e.append({id:t.id,title:t.title,errors:[gettext("You don't have permission to merge this thread with others.")]})}),n.props.threads.length<2)O["default"].info(gettext("You have to select at least two threads to merge."));else{if(e.length)return void E["default"].show(c["default"].createElement(f["default"],{errors:e}));E["default"].show(c["default"].createElement(m["default"],n.props))}},n["delete"]=function(){if(confirm(gettext("Are you sure you want to delete selected threads?"))){n.props.threads.map(function(e){n.props.freezeThread(e.id)});var e=n.props.threads.map(function(e){return e.id});g["default"]["delete"](n.props.api,e).then(function(){n.props.threads.map(function(e){n.props.freezeThread(e.id),n.props.deleteThread(e)}),O["default"].success(gettext("Selected threads were deleted."))},function(e){if(400===e.status){var t=e.map(function(e){return e.id});n.props.threads.map(function(e){n.props.freezeThread(e.id),t.indexOf(e.id)===-1&&n.props.deleteThread(e)}),E["default"].show(c["default"].createElement(f["default"],{errors:e}))}else O["default"].apiError(e)})}},r=a,l(n,r)}return s(t,e),i(t,[{key:"getPinGloballyButton",value:function(){return this.props.moderation.can_pin_globally?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinGlobally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark"),gettext("Pin threads globally"))):null}},{key:"getPinLocallyButton",value:function(){return this.props.moderation.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinLocally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark_border"),gettext("Pin threads locally"))):null}},{key:"getUnpinButton",value:function(){return this.props.moderation.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unpin,type:"button"},c["default"].createElement("span",{className:"material-icon"},"panorama_fish_eye"),gettext("Unpin threads"))):null}},{key:"getMoveButton",value:function(){return this.props.moderation.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.move,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move threads"))):null}},{key:"getMergeButton",value:function(){return this.props.moderation.can_merge?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.merge,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge threads"))):null}},{key:"getApproveButton",value:function(){return this.props.moderation.can_approve?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.approve,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve threads"))):null}},{key:"getOpenButton",value:function(){return this.props.moderation.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.open,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Open threads"))):null}},{key:"getCloseButton",value:function(){return this.props.moderation.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.close,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Close threads"))):null}},{key:"getUnhideButton",value:function(){return this.props.moderation.can_unhide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unhide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide threads"))):null}},{key:"getHideButton",value:function(){return this.props.moderation.can_hide?c["default"].createElement("li",null,c["default"].createElement("button",{onClick:this.hide,type:"button",className:"btn btn-link"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide threads"))):null}},{key:"getDeleteButton",value:function(){return this.props.moderation.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this["delete"],type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete threads"))):null}},{key:"render",value:function(){return c["default"].createElement("ul",{className:this.props.className},this.getPinGloballyButton(),this.getPinLocallyButton(),this.getUnpinButton(),this.getMoveButton(),this.getMergeButton(),this.getApproveButton(),this.getOpenButton(),this.getCloseButton(),this.getUnhideButton(),this.getHideButton(),this.getDeleteButton())}}]),t}(c["default"].Component));a["default"]=x},{"../../../reducers/selection":354,"../../../services/ajax":361,"../../../services/modal":367,"../../../services/snackbar":372,"../../../services/store":373,"../../../utils/countdown":378,"./errors-list":259,"./merge":260,"./move":261,react:"react"}],259:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.errors,a=e.thread;return c["default"].createElement("li",null,c["default"].createElement("h5",null,a.title),t.map(function(e,t){return c["default"].createElement("p",null,e)}))}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ThreadErrors=s;var u=e("react"),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("div",{className:"modal-dialog",role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Threads moderation"))),c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("p",{className:"lead"},gettext("One or more threads could not be deleted:")),c["default"].createElement("ul",{className:"list-unstyled list-errored-items"},this.props.errors.map(function(e){return c["default"].createElement(s,{errors:e.errors,key:e.thread.id,thread:e.thread})})))))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],260:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../category-select"),_=r(v),g=e("../../select"),y=r(g),E=e("../../../index"),w=r(E),O=e("../../../reducers/threads"),k=e("../../../reducers/selection"),N=n(k),x=e("./errors-list"),j=r(x),P=e("../../merge-conflict"),C=r(P),M=e("../../../services/ajax"),S=r(M),T=e("../../../services/modal"),L=r(T),A=e("../../../services/snackbar"),R=r(A),I=e("../../../services/store"),D=r(I),U=e("../../../utils/validators"),B=n(U),H=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.getFormdata=function(){return{threads:a.props.threads.map(function(e){return e.id}),title:a.state.title,category:a.state.category,weight:a.state.weight,is_hidden:a.state.is_hidden,is_closed:a.state.is_closed}},a.handleSuccess=function(e){a.props.threads.forEach(function(e){a.props.freezeThread(e.id),a.props.deleteThread(e)}),D["default"].dispatch(N.none()),a.props.addThreads([e]),D["default"].dispatch((0,O.filterThreads)(a.props.route.category,a.props.categoriesMap)),L["default"].hide()},a.handleError=function(e){400===e.status?e.best_answers||e.polls?L["default"].show(c["default"].createElement(C["default"],{api:w["default"].get("MERGE_THREADS_API"),bestAnswers:e.best_answers,data:a.getFormdata(),polls:e.polls,onError:a.handleError,onSuccess:a.handleSuccess})):(a.setState({errors:Object.assign({},a.state.errors,e)}),R["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?L["default"].show(c["default"].createElement(j["default"],{errors:e})):e.best_answer?R["default"].error(e.best_answer[0]):e.poll?R["default"].error(e.poll[0]):R["default"].apiError(e)},a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,weight:0,is_hidden:0,is_closed:!1,validators:{title:[B.required()]},errors:{}},a.acl={};for(var n in e.user.acl.categories)if(e.user.acl.categories.hasOwnProperty(n)){var r=e.user.acl.categories[n];a.acl[r.id]=r}return a.categoryChoices=[],e.categories.forEach(function(e){if(e.level>0){var t=a.acl[e.id],n=!t.can_start_threads||e.is_closed&&!t.can_close_threads;a.categoryChoices.push({value:e.id,disabled:n,level:e.level-1,label:e.name}),n||a.state.category||(a.state.category=e.id)}}),a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(R["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return S["default"].post(w["default"].get("MERGE_THREADS_API"),this.getFormdata())}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?c["default"].createElement(b["default"],{label:gettext("Thread weight"),"for":"id_weight"},c["default"].createElement(y["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?c["default"].createElement(b["default"],{label:gettext("Hide thread"),"for":"id_is_hidden"},c["default"].createElement(y["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?c["default"].createElement(b["default"],{label:gettext("Close thread"),"for":"id_is_closed"},c["default"].createElement(y["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"renderForm",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(b["default"],{label:gettext("Thread title"),"for":"id_title",validation:this.state.errors.title},c["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),c["default"].createElement("div",{className:"clearfix"}),c["default"].createElement(b["default"],{label:gettext("Category"),"for":"id_category",validation:this.state.errors.category},c["default"].createElement(_["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.categoryChoices})),c["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Merge threads"))))}},{key:"renderCantMergeMessage",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("You can't move threads because there are no categories you are allowed to move them to.")),c["default"].createElement("p",null,gettext("You need permission to start threads in category to be able to merge threads to it.")),c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}},{key:"getClassName",value:function(){return this.state.category?"modal-dialog":"modal-dialog modal-message"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Merge threads"))),this.state.category?this.renderForm():this.renderCantMergeMessage()))}}]),t}(m["default"]);a["default"]=H},{"../../../index":299,"../../../reducers/selection":354,"../../../reducers/threads":357,"../../../services/ajax":361,"../../../services/modal":367,"../../../services/snackbar":372,"../../../services/store":373,"../../../utils/validators":389,"../../button":7,"../../category-select":20,"../../form":54,"../../form-group":53,"../../merge-conflict":57,"../../select":207,"./errors-list":259,react:"react"}],261:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../form"),f=r(d),p=e("../../form-group"),m=r(p),h=e("../../category-select"),b=r(h),v=e("../../../reducers/selection"),_=n(v),g=e("../../../reducers/threads"),y=e("../../../services/modal"),E=r(y),w=e("../../../services/store"),O=r(w),k=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.handleSubmit=function(e){e.preventDefault(),E["default"].hide();var t=function(){O["default"].dispatch((0,g.filterThreads)(a.props.route.category,a.props.categoriesMap));var e=O["default"].getState(),t=e.threads.map(function(e){return e.id});O["default"].dispatch(_.all(e.selection.filter(function(e){return t.indexOf(e)!==-1})))};a.props.callApi([{op:"replace",path:"category",value:a.state.category},{op:"replace",path:"flatten-categories",value:null},{op:"add",path:"acl",value:!0}],gettext("Selected threads were moved."),t)},a.state={category:null};var n={};for(var r in e.user.acl.categories)if(e.user.acl.categories.hasOwnProperty(r)){var s=e.user.acl.categories[r];n[s.id]=s}return a.categoryChoices=[],e.categories.forEach(function(e){if(e.level>0){var t=n[e.id],r=!t.can_start_threads||e.is_closed&&!t.can_close_threads;a.categoryChoices.push({value:e.id,disabled:r,level:e.level-1,label:e.name}),r||a.state.category||(a.state.category=e.id)}}),a}return s(t,e),i(t,[{key:"getClassName",value:function(){return this.state.category?"modal-dialog":"modal-dialog modal-message"}},{key:"renderForm",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(m["default"],{label:gettext("New category"),"for":"id_new_category"},c["default"].createElement(b["default"],{id:"id_new_category",onChange:this.bindInput("category"),value:this.state.category,choices:this.categoryChoices}))),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement("button",{className:"btn btn-primary"},gettext("Move threads"))))}},{key:"renderCantMoveMessage",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("You can't move threads because there are no categories you are allowed to move them to.")),c["default"].createElement("p",null,gettext("You need permission to start threads in category to be able to move threads to it.")),c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Move threads"))),this.state.category?this.renderForm():this.renderCantMoveMessage()))}}]),t}(f["default"]);a["default"]=k},{"../../../reducers/selection":354,"../../../reducers/threads":357,"../../../services/modal":367,"../../../services/store":373,"../../category-select":20,"../../form":54,"../../form-group":53,react:"react"}],262:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../../reducers/selection"),f=n(d),p=e("../../../services/store"),m=r(p),h=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.selectAll=function(){m["default"].dispatch(f.all(n.props.threads.map(function(e){return e.id})))},n.selectNone=function(){m["default"].dispatch(f.none())},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("ul",{className:this.props.className},c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",type:"button",onClick:this.selectAll},c["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Select all"))),c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",type:"button",onClick:this.selectNone},c["default"].createElement("span",{className:"material-icon"},"check_box_outline_blank"),gettext("Select none"))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../reducers/selection":354,"../../../services/store":373,react:"react"}],263:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.baseUrl,a=e.list,n=e.lists;return n.length<2?null:o["default"].createElement("div",{className:"page-tabs"},o["default"].createElement("div",{className:"container"},o["default"].createElement("ul",{className:"nav nav-pills"},n.map(function(e){return o["default"].createElement(i["default"],{isControlled:!0,isActive:e.path===a.path,key:t+e.path},o["default"].createElement(l.Link,{to:t+e.path},e.name))}))))};var r=e("react"),o=n(r),l=e("react-router"),s=e("../li"),i=n(s)},{"../li":55,react:"react","react-router":"react-router"}],264:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return function(t){return{options:e,selection:t.selection,threads:t.threads,tick:t.tick.tick,user:t.auth.user}}}function o(e){var t=[{type:"all",path:"",name:gettext("All"),longName:gettext("All threads")}];return e.id&&(t.push({type:"my",path:"my/",name:gettext("My"),longName:gettext("My threads")}),t.push({type:"new",path:"new/",name:gettext("New"),longName:gettext("New threads")}),t.push({type:"unread",path:"unread/",name:gettext("Unread"),longName:gettext("Unread threads")}),t.push({type:"subscribed",path:"subscribed/",name:gettext("Subscribed"),longName:gettext("Subscribed threads")}),e.acl.can_see_unapproved_content_lists&&t.push({type:"unapproved",path:"unapproved/",name:gettext("Unapproved"),longName:gettext("Unapproved content")})),t}function l(e,t){var a=o(e),n=[],l={};return d["default"].get("CATEGORIES").forEach(function(e){a.forEach(function(o){l[e.id]=e,n.push({path:e.url.index+o.path,component:(0,s.connect)(r(t))(u["default"]),categories:d["default"].get("CATEGORIES"),categoriesMap:l,category:e,lists:a,list:o})})}),n}Object.defineProperty(a,"__esModule",{value:!0}),a.getSelect=r,a.getLists=o,a.paths=l;var s=e("react-redux"),i=e("./route"),u=n(i),c=e("../../index"),d=n(c)},{"../../index":299,"./route":265,"react-redux":"react-redux"}],265:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);
-return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../button"),f=r(d),p=e("./compare"),m=e("./container"),h=r(m),b=e("./header"),v=r(b),_=e("./utils"),g=e("../threads-list"),y=r(g),E=e("./list-empty"),w=r(E),O=e("../with-dropdown"),k=r(O),N=e("../../index"),x=r(N),j=e("../../reducers/selection"),P=n(j),C=e("../../reducers/threads"),M=e("../../services/ajax"),S=r(M),T=e("../../services/polls"),L=r(T),A=e("../../services/snackbar"),R=r(A),I=e("../../services/store"),D=r(I),U=e("../../services/page-title"),B=r(U),H=e("../../utils/sets"),z=n(H),F=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.loadMore=function(){a.setState({isBusy:!0}),a.loadThreads(a.getCategory(),a.state.page+1)},a.pollResponse=function(e){a.setState({diff:Object.assign({},e,{results:(0,_.diffThreads)(a.props.threads,e.results)})})},a.addThreads=function(e){D["default"].dispatch((0,C.append)(e,a.getSorting()))},a.applyDiff=function(){a.addThreads(a.state.diff.results),a.setState(Object.assign({},a.state.diff,{moderation:(0,_.getModerationActions)(D["default"].getState().threads),diff:{results:[]}}))},a.freezeThread=function(e){a.setState(function(t){return{busyThreads:z.toggle(t.busyThreads,e)}})},a.updateThread=function(e){D["default"].dispatch((0,C.patch)(e,e,a.getSorting()))},a.deleteThread=function(e){D["default"].dispatch((0,C.deleteThread)(e))},a.state={isMounted:!0,isLoaded:!1,isBusy:!1,diff:{results:[]},moderation:[],busyThreads:[],dropdown:!1,subcategories:[],count:0,more:0,page:1,pages:1};var n=a.getCategory();return x["default"].has("THREADS")?a.initWithPreloadedData(n,x["default"].get("THREADS")):a.initWithoutPreloadedData(n),a}return s(t,e),i(t,[{key:"getCategory",value:function(){return this.props.route.category.special_role?null:this.props.route.category.id}},{key:"initWithPreloadedData",value:function(e,t){this.state=Object.assign(this.state,{moderation:(0,_.getModerationActions)(t.results),subcategories:t.subcategories,count:t.count,more:t.more,page:t.page,pages:t.pages}),this.startPolling(e)}},{key:"initWithoutPreloadedData",value:function(e){this.loadThreads(e)}},{key:"loadThreads",value:function(e){var t=this,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;S["default"].get(this.props.options.api,{category:e,list:this.props.route.list.type,page:a||1},"threads").then(function(n){t.state.isMounted&&(1===a?D["default"].dispatch((0,C.hydrate)(n.results)):D["default"].dispatch((0,C.append)(n.results,t.getSorting())),t.setState({isLoaded:!0,isBusy:!1,moderation:(0,_.getModerationActions)(D["default"].getState().threads),subcategories:n.subcategories,count:n.count,more:n.more,page:n.page,pages:n.pages}),t.startPolling(e))},function(e){R["default"].apiError(e)})}},{key:"startPolling",value:function(e){L["default"].start({poll:"threads",url:this.props.options.api,data:{category:e,list:this.props.route.list.type},frequency:12e4,update:this.pollResponse})}},{key:"componentDidMount",value:function(){this.setPageTitle(),x["default"].has("THREADS")&&(D["default"].dispatch((0,C.hydrate)(x["default"].pop("THREADS").results)),this.setState({isLoaded:!0})),D["default"].dispatch(P.none())}},{key:"componentWillUnmount",value:function(){this.state.isMounted=!1,L["default"].stop("threads")}},{key:"getTitle",value:function(){return this.props.options.title?this.props.options.title:(0,_.getTitle)(this.props.route)}},{key:"setPageTitle",value:function(){this.props.route.category.level||!x["default"].get("THREADS_ON_INDEX")?B["default"].set((0,_.getPageTitle)(this.props.route)):this.props.options.title?B["default"].set(this.props.options.title):x["default"].get("SETTINGS").forum_index_title?document.title=x["default"].get("SETTINGS").forum_index_title:document.title=x["default"].get("SETTINGS").forum_name}},{key:"getSorting",value:function(){return this.props.route.category.level?p.compareWeight:p.compareGlobalWeight}},{key:"getMoreButton",value:function(){return this.state.more?c["default"].createElement("div",{className:"pager-more"},c["default"].createElement(f["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy||this.state.busyThreads.length,onClick:this.loadMore},gettext("Show more"))):null}},{key:"getClassName",value:function(){var e="page page-threads";return e+=" page-threads-"+this.props.route.list.type,this.props.route.category.css_class&&(e+=" page-threads-"+this.props.route.category.css_class),e}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName()},c["default"].createElement(v["default"],{categories:this.props.route.categoriesMap,disabled:!this.state.isLoaded,startThread:this.props.options.startThread,threads:this.props.threads,title:this.getTitle(),toggleNav:this.toggleNav,route:this.props.route,user:this.props.user}),c["default"].createElement(h["default"],{api:this.props.options.api,route:this.props.route,subcategories:this.state.subcategories,user:this.props.user,pageLead:this.props.options.pageLead,threads:this.props.threads,threadsCount:this.state.count,moderation:this.state.moderation,selection:this.props.selection,busyThreads:this.state.busyThreads,addThreads:this.addThreads,freezeThread:this.freezeThread,deleteThread:this.deleteThread,updateThread:this.updateThread,isLoaded:this.state.isLoaded,isBusy:this.state.isBusy},c["default"].createElement(y["default"],{category:this.props.route.category,categories:this.props.route.categoriesMap,list:this.props.route.list,selection:this.props.selection,threads:this.props.threads,diffSize:this.state.diff.results.length,applyDiff:this.applyDiff,showOptions:!!this.props.user.id,isLoaded:this.state.isLoaded,busyThreads:this.state.busyThreads},c["default"].createElement(w["default"],{category:this.props.route.category,emptyMessage:this.props.options.emptyMessage,list:this.props.route.list})),this.getMoreButton()))}}]),t}(k["default"]);a["default"]=F},{"../../index":299,"../../reducers/selection":354,"../../reducers/threads":357,"../../services/ajax":361,"../../services/page-title":369,"../../services/polls":370,"../../services/snackbar":372,"../../services/store":373,"../../utils/sets":387,"../button":7,"../threads-list":235,"../with-dropdown":296,"./compare":254,"./container":255,"./header":256,"./list-empty":257,"./utils":267,react:"react"}],266:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./category-picker"),d=n(c),f=e("./moderation/controls"),p=n(f),m=e("./moderation/selection"),h=n(m),b=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getCategoryPicker",value:function(){return this.props.subcategories.length?u["default"].createElement(d["default"],{categories:this.props.categoriesMap,choices:this.props.subcategories,list:this.props.list}):null}},{key:"showModerationOptions",value:function(){return this.props.user.id&&this.props.moderation.allow}},{key:"getSelectedThreads",value:function(){var e=this;return this.props.threads.filter(function(t){return e.props.selection.indexOf(t.id)>=0})}},{key:"getModerationButton",value:function(){return this.showModerationOptions()?u["default"].createElement("div",{className:"col-xs-6 col-sm-3 col-md-2"},u["default"].createElement("div",{className:"btn-group btn-group-justified"},u["default"].createElement("div",{className:"btn-group dropdown"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false",disabled:this.props.disabled||!this.props.selection.length},u["default"].createElement("span",{className:"material-icon"},"settings"),gettext("Options")),u["default"].createElement(p["default"],{addThreads:this.props.addThreads,api:this.props.api,categories:this.props.categories,categoriesMap:this.props.categoriesMap,className:"dropdown-menu dropdown-menu-right stick-to-bottom",deleteThread:this.props.deleteThread,freezeThread:this.props.freezeThread,moderation:this.props.moderation,route:this.props.route,threads:this.getSelectedThreads(),updateThread:this.props.updateThread,user:this.props.user})))):null}},{key:"getSelectionButton",value:function(){return this.showModerationOptions()?u["default"].createElement("div",{className:"col-xs-3 col-sm-2 col-md-1"},u["default"].createElement("div",{className:"btn-group btn-group-justified"},u["default"].createElement("div",{className:"btn-group dropdown"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline btn-icon dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false",disabled:this.props.disabled},u["default"].createElement("span",{className:"material-icon"},"select_all")),u["default"].createElement(h["default"],{className:"dropdown-menu dropdown-menu-right stick-to-bottom",threads:this.props.threads})))):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"row row-toolbar row-toolbar-bottom-margin"},u["default"].createElement("div",{className:"col-xs-3 col-sm-3 col-md-2 dropdown"},this.getCategoryPicker()),u["default"].createElement("div",{className:"hidden-xs col-sm-4 col-md-7"}),this.getModerationButton(),this.getSelectionButton())}}]),t}(u["default"].Component);a["default"]=b},{"./category-picker":253,"./moderation/controls":258,"./moderation/selection":262,react:"react"}],267:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.category.level?e.list.path?{title:e.list.longName,parent:e.category.name}:{title:e.category.name}:c["default"].get("THREADS_ON_INDEX")?e.list.path?{title:e.list.longName}:null:e.list.path?{title:e.list.longName,parent:gettext("Threads")}:{title:gettext("Threads")}}function o(e){return e.category.level?e.category.name:c["default"].get("THREADS_ON_INDEX")?c["default"].get("SETTINGS").forum_index_title?c["default"].get("SETTINGS").forum_index_title:c["default"].get("SETTINGS").forum_name:gettext("Threads")}function l(e,t){return[e.title===t.title,e.weight===t.weight,e.category===t.category,e.last_post===t.last_post,e.last_poster_name===t.last_poster_name].indexOf(!1)>=0}function s(e,t){var a={};return e.forEach(function(e){a[e.id]=e}),t.filter(function(e){return!a[e.id]||l(a[e.id],e)})}function i(e){var t={allow:!1,can_approve:0,can_close:0,can_delete:0,can_hide:0,can_merge:0,can_move:0,can_pin:0,can_pin_globally:0,can_unhide:0};return e.forEach(function(e){e.is_unapproved&&e.acl.can_approve>t.can_approve&&(t.can_approve=e.acl.can_approve),e.acl.can_close>t.can_close&&(t.can_close=e.acl.can_close),e.acl.can_delete>t.can_delete&&(t.can_delete=e.acl.can_delete),e.acl.can_hide>t.can_hide&&(t.can_hide=e.acl.can_hide),e.acl.can_merge>t.can_merge&&(t.can_merge=e.acl.can_merge),e.acl.can_move>t.can_move&&(t.can_move=e.acl.can_move),e.acl.can_pin>t.can_pin&&(t.can_pin=e.acl.can_pin),e.acl.can_pin_globally>t.can_pin_globally&&(t.can_pin_globally=e.acl.can_pin_globally),e.acl.can_unhide>t.can_unhide&&(t.can_unhide=e.acl.can_unhide),t.allow=t.can_approve||t.can_close||t.can_delete||t.can_hide||t.can_merge||t.can_move||t.can_pin||t.can_pin_globally||t.can_unhide}),t}Object.defineProperty(a,"__esModule",{value:!0}),a.getPageTitle=r,a.getTitle=o,a.isThreadChanged=l,a.diffThreads=s,a.getModerationActions=i;var u=e("../../index"),c=n(u)},{"../../index":299}],268:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactGuestNav=a.GuestNav=a.GuestMenu=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=e("../navbar-search"),p=n(f),m=e("../register-button"),h=n(m),b=e("../sign-in.js"),v=n(b),_=e("../../services/mobile-navbar-dropdown"),g=n(_),y=e("../../services/modal"),E=n(y),w=a.GuestMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"showSignInModal",value:function(){E["default"].show(v["default"])}},{key:"render",value:function(){return u["default"].createElement("ul",{className:"dropdown-menu user-dropdown dropdown-menu-right",role:"menu"},u["default"].createElement("li",{className:"guest-preview"},u["default"].createElement("h4",null,gettext("You are browsing as guest.")),u["default"].createElement("p",null,gettext("Sign in or register to start and participate in discussions.")),u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-xs-6"},u["default"].createElement("button",{className:"btn btn-default btn-sign-in btn-block",onClick:this.showSignInModal,type:"button"},gettext("Sign in"))),u["default"].createElement("div",{className:"col-xs-6"},u["default"].createElement(h["default"],{className:"btn-primary btn-register btn-block"},gettext("Register"))))))}}]),t}(u["default"].Component);a.GuestNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"nav nav-guest"},u["default"].createElement("button",{className:"btn navbar-btn btn-default btn-sign-in",onClick:this.showSignInModal,type:"button"},gettext("Sign in")),u["default"].createElement(h["default"],{className:"navbar-btn btn-primary btn-register"},gettext("Register")),u["default"].createElement("div",{className:"navbar-left"},u["default"].createElement(p["default"],null)))}}]),t}(w),a.CompactGuestNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"showGuestMenu",value:function(){g["default"].show(w)}},{key:"render",value:function(){return u["default"].createElement("button",{type:"button",onClick:this.showGuestMenu},u["default"].createElement(d["default"],{size:"64"}))}}]),t}(u["default"].Component)},{"../../services/mobile-navbar-dropdown":366,"../../services/modal":367,"../avatar":5,"../navbar-search":75,"../register-button":195,"../sign-in.js":208,react:"react"}],269:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.auth}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactUserMenu=a.UserMenu=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=e("./guest-nav"),f=e("./user-nav");a.UserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return this.props.isAuthenticated?c["default"].createElement(f.UserNav,{user:this.props.user}):c["default"].createElement(d.GuestNav,null)}}]),t}(c["default"].Component),a.CompactUserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return this.props.isAuthenticated?c["default"].createElement(f.CompactUserNav,{user:this.props.user}):c["default"].createElement(d.CompactGuestNav,null)}}]),t}(c["default"].Component)},{"./guest-nav":268,"./user-nav":270,react:"react"}],270:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.user;return t.unread_private_threads?p["default"].createElement("span",{className:"badge"},t.unread_private_threads):null}function i(e){var t=e.user;return p["default"].createElement("ul",{className:"ul nav navbar-nav nav-user"},p["default"].createElement("li",null,p["default"].createElement(y["default"],null)),p["default"].createElement(u,{user:t}),p["default"].createElement("li",{className:"dropdown"},p["default"].createElement("a",{"aria-haspopup":"true","aria-expanded":"false",className:"dropdown-toggle","data-toggle":"dropdown",href:t.url,role:"button"},p["default"].createElement(b["default"],{user:t,size:"64"})),p["default"].createElement(j,{user:t})))}function u(e){var t=e.user;if(!t.acl.can_use_private_threads)return null;var a=null;return a=t.unread_private_threads?gettext("You have unread private threads!"):gettext("Private threads"),p["default"].createElement("li",null,p["default"].createElement("a",{className:"navbar-icon",href:w["default"].get("PRIVATE_THREADS_URL"),title:a},p["default"].createElement("span",{className:"material-icon"},"message"),t.unread_private_threads>0&&p["default"].createElement("span",{className:"badge"},t.unread_private_threads)))}function c(e){return{user:e.auth.user}}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactUserNav=a.UserMenu=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.PrivateThreadsBadge=s,a.UserNav=i,a.UserPrivateThreadsLink=u,a.selectUserMenu=c;var f=e("react"),p=n(f),m=e("react-redux"),h=e("../avatar"),b=n(h),v=e("../change-avatar/root"),_=n(v),g=e("../navbar-search"),y=n(g),E=e("../.."),w=n(E),O=e("../../services/mobile-navbar-dropdown"),k=n(O),N=e("../../services/modal"),x=n(N),j=a.UserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),d(t,[{key:"logout",value:function(){var e=confirm(gettext("Are you sure you want to sign out?"));e&&$("#hidden-logout-form").submit()}},{key:"changeAvatar",value:function(){x["default"].show((0,m.connect)(v.select)(_["default"]))}},{key:"render",value:function(){var e=this.props.user;return p["default"].createElement("ul",{className:"dropdown-menu user-dropdown dropdown-menu-right",role:"menu"},p["default"].createElement("li",{className:"dropdown-header"},p["default"].createElement("strong",null,e.username),p["default"].createElement("ul",{className:"list-unstyled list-inline user-stats"},p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"message"),e.posts),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"forum"),e.threads),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"favorite"),e.followers),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"favorite_outline"),e.following))),p["default"].createElement("li",{className:"divider"}),p["default"].createElement("li",null,p["default"].createElement("a",{href:e.url},p["default"].createElement("span",{className:"material-icon"},"account_circle"),gettext("See your profile"))),p["default"].createElement("li",null,p["default"].createElement("a",{href:w["default"].get("USERCP_URL")},p["default"].createElement("span",{className:"material-icon"},"done_all"),gettext("Change options"))),p["default"].createElement("li",null,p["default"].createElement("button",{className:"btn-link",onClick:this.changeAvatar,type:"button"},p["default"].createElement("span",{className:"material-icon"},"portrait"),gettext("Change avatar"))),!!e.acl.can_use_private_threads&&p["default"].createElement("li",null,p["default"].createElement("a",{href:w["default"].get("PRIVATE_THREADS_URL")},p["default"].createElement("span",{className:"material-icon"},"message"),gettext("Private threads"),p["default"].createElement(s,{user:e}))),p["default"].createElement("li",{className:"divider"}),p["default"].createElement("li",{className:"dropdown-buttons"},p["default"].createElement("button",{className:"btn btn-default btn-block",onClick:this.logout,type:"button"},gettext("Log out"))))}}]),t}(p["default"].Component);a.CompactUserNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),d(t,[{key:"showUserMenu",value:function(){k["default"].showConnected("user-menu",(0,m.connect)(c)(j))}},{key:"render",value:function(){return p["default"].createElement("button",{type:"button",onClick:this.showUserMenu},p["default"].createElement(b["default"],{user:this.props.user,size:"50"}))}}]),t}(p["default"].Component)},{"../..":299,"../../services/mobile-navbar-dropdown":366,"../../services/modal":367,"../avatar":5,"../change-avatar/root":24,"../navbar-search":75,react:"react","react-redux":"react-redux"}],271:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t="";return e.is_banned?t="banned":e.is_hidden?t="offline":e.is_online_hidden?t="online":e.is_offline_hidden?t="offline":e.is_online?t="online":e.is_offline&&(t="offline"),"user-status user-"+t}function i(e,t){return t.is_banned?t.banned_until?interpolate(gettext("%(username)s is banned until %(ban_expires)s"),{username:e.username,ban_expires:t.banned_until.format("LL, LT")},!0):interpolate(gettext("%(username)s is banned"),{username:e.username},!0):t.is_hidden?interpolate(gettext("%(username)s is hiding presence"),{username:e.username},!0):t.is_online_hidden?interpolate(gettext("%(username)s is online (hidden)"),{username:e.username},!0):t.is_offline_hidden?interpolate(gettext("%(username)s was last seen %(last_click)s (hidden)"),{username:e.username,last_click:t.last_click.fromNow()},!0):t.is_online?interpolate(gettext("%(username)s is online"),{username:e.username},!0):t.is_offline?interpolate(gettext("%(username)s was last seen %(last_click)s"),{username:e.username,last_click:t.last_click.fromNow()},!0):void 0}Object.defineProperty(a,"__esModule",{value:!0}),a.StatusLabel=a.StatusIcon=void 0;var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getStatusClassName=s,a.getStatusDescription=i;var c=e("react"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getClass",value:function(){return s(this.props.status)}},{key:"render",value:function(){return d["default"].createElement("span",{className:this.getClass()},this.props.children)}}]),t}(d["default"].Component);a["default"]=f;a.StatusIcon=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getIcon",value:function(){return this.props.status.is_banned?"remove_circle_outline":this.props.status.is_hidden?"help_outline":this.props.status.is_online_hidden?"label":this.props.status.is_offline_hidden?"label_outline":this.props.status.is_online?"lens":this.props.status.is_offline?"panorama_fish_eye":void 0}},{key:"render",value:function(){return d["default"].createElement("span",{className:"material-icon status-icon"},this.getIcon())}}]),t}(d["default"].Component),a.StatusLabel=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getHelp",value:function(){return i(this.props.user,this.props.status)}},{key:"getLabel",value:function(){return this.props.status.is_banned?gettext("Banned"):this.props.status.is_hidden?gettext("Hidden"):this.props.status.is_online_hidden?gettext("Online (hidden)"):this.props.status.is_offline_hidden?gettext("Offline (hidden)"):this.props.status.is_online?gettext("Online"):this.props.status.is_offline?gettext("Offline"):void 0}},{key:"render",value:function(){return d["default"].createElement("span",{className:this.props.className||"status-label",title:this.getHelp()},this.getLabel())}}]),t}(d["default"].Component)},{react:"react"}],272:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../avatar"),f=r(d),p=e("../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"getClassName",value:function(){return this.props.hiddenOnMobile?"list-group-item hidden-xs hidden-sm":"list-group-item"}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"change-avatar"},c["default"].createElement("span",{className:"user-avatar"},c["default"].createElement(f["default"],{size:"100"}))),c["default"].createElement("div",{className:"change-author"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,100)+"px"}}," ")),c["default"].createElement("div",{className:"change"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," "),c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("div",{className:"change-date"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](80,140)+"px"}}," ")))}}]),t}(c["default"].Component);a["default"]=h},{"../../utils/random":384,"../avatar":5,react:"react"}],273:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"renderUserAvatar",value:function(){return this.props.change.changed_by?u["default"].createElement("a",{
-href:this.props.change.changed_by.url,className:"user-avatar-wrapper"},u["default"].createElement(d["default"],{user:this.props.change.changed_by,size:"100"})):u["default"].createElement("span",{className:"user-avatar-wrapper"},u["default"].createElement(d["default"],{size:"100"}))}},{key:"renderUsername",value:function(){return this.props.change.changed_by?u["default"].createElement("a",{href:this.props.change.changed_by.url,className:"item-title"},this.props.change.changed_by.username):u["default"].createElement("span",{className:"item-title"},this.props.change.changed_by_username)}},{key:"render",value:function(){return u["default"].createElement("li",{className:"list-group-item",key:this.props.change.id},u["default"].createElement("div",{className:"change-avatar"},this.renderUserAvatar()),u["default"].createElement("div",{className:"change-author"},this.renderUsername()),u["default"].createElement("div",{className:"change"},u["default"].createElement("span",{className:"old-username"},this.props.change.old_username),u["default"].createElement("span",{className:"material-icon"},"arrow_forward"),u["default"].createElement("span",{className:"new-username"},this.props.change.new_username)),u["default"].createElement("div",{className:"change-date"},u["default"].createElement("abbr",{title:this.props.change.changed_on.format("LLL")},this.props.change.changed_on.fromNow())))}}]),t}(u["default"].Component);a["default"]=f},{"../avatar":5,react:"react"}],274:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getEmptyMessage",value:function(){return this.props.emptyMessage?this.props.emptyMessage:gettext("No name changes have been recorded for your account.")}},{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-ready"},u["default"].createElement("ul",{className:"list-group"},u["default"].createElement("li",{className:"list-group-item empty-message"},this.getEmptyMessage())))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],275:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change-preview"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-preview"},u["default"].createElement("ul",{className:"list-group"},[0,1,2].map(function(e){return u["default"].createElement(d["default"],{hiddenOnMobile:e>0,key:e})})))}}]),t}(u["default"].Component);a["default"]=f},{"./change-preview":272,react:"react"}],276:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.props.changes.map(function(e){return u["default"].createElement(d["default"],{change:e,key:e.id})})))}}]),t}(u["default"].Component);a["default"]=f},{"./change":273,react:"react"}],277:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-empty"),d=n(c),f=e("./list-ready"),p=n(f),m=e("./list-preview"),h=n(m),b=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return this.props.isLoaded?this.props.changes.length?u["default"].createElement(p["default"],{changes:this.props.changes}):u["default"].createElement(d["default"],{emptyMessage:this.props.emptyMessage}):u["default"].createElement(h["default"],null)}}]),t}(u["default"].Component);a["default"]=b},{"./list-empty":274,"./list-preview":275,"./list-ready":276,react:"react"}],278:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.showStatus,a=e.user,n=a.rank,r="panel user-card";return n.css_class&&(r+=" user-card-"+n.css_class),o["default"].createElement("div",{className:r},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-3 user-card-left"},o["default"].createElement("div",{className:"user-card-small-avatar"},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{size:"50",size2x:"80",user:a})))),o["default"].createElement("div",{className:"col-xs-9 col-sm-12 user-card-body"},o["default"].createElement("div",{className:"user-card-avatar"},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{size:"150",size2x:"200",user:a}))),o["default"].createElement("div",{className:"user-card-username"},o["default"].createElement("a",{href:a.url},a.username)),o["default"].createElement("div",{className:"user-card-title"},o["default"].createElement(d["default"],{rank:n,title:a.title})),o["default"].createElement("div",{className:"user-card-stats"},o["default"].createElement(u["default"],{showStatus:t,user:a}))))))};var r=e("react"),o=n(r),l=e("../../avatar"),s=n(l),i=e("./stats"),u=n(i),c=e("./user-title"),d=n(c)},{"../../avatar":5,"./stats":279,"./user-title":280,react:"react"}],279:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.showStatus,a=e.user;return t?d["default"].createElement("li",{className:"user-stat-status"},d["default"].createElement(p["default"],{status:a.status},d["default"].createElement(f.StatusLabel,{status:a.status,user:a}))):null}function o(e){var t=e.user,a=t.joined_on,n=interpolate(gettext("Joined on %(joined_on)s"),{joined_on:a.format("LL, LT")},!0),r=interpolate(gettext("Joined %(joined_on)s"),{joined_on:a.fromNow()},!0);return d["default"].createElement("li",{className:"user-stat-join-date"},d["default"].createElement("abbr",{title:n},r))}function l(e){var t=e.user,a=u("user-stat-posts",t.posts),n=ngettext("%(posts)s post","%(posts)s posts",t.posts);return d["default"].createElement("li",{className:a},interpolate(n,{posts:t.posts},!0))}function s(e){var t=e.user,a=u("user-stat-threads",t.threads),n=ngettext("%(threads)s thread","%(threads)s threads",t.threads);return d["default"].createElement("li",{className:a},interpolate(n,{threads:t.threads},!0))}function i(e){var t=e.user,a=u("user-stat-followers",t.followers),n=ngettext("%(followers)s follower","%(followers)s followers",t.followers);return d["default"].createElement("li",{className:a},interpolate(n,{followers:t.followers},!0))}function u(e,t){return 0===t?e+" user-stat-empty":e}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.showStatus,a=e.user;return d["default"].createElement("ul",{className:"list-unstyled"},d["default"].createElement(r,{showStatus:t,user:a}),d["default"].createElement(o,{user:a}),d["default"].createElement("li",{className:"user-stat-divider"}),d["default"].createElement(l,{user:a}),d["default"].createElement(s,{user:a}),d["default"].createElement(i,{user:a}))},a.Status=r,a.JoinDate=o,a.Posts=l,a.Threads=s,a.Followers=i,a.getStatClassName=u;var c=e("react"),d=n(c),f=e("../../user-status"),p=n(f)},{"../../user-status":271,react:"react"}],280:[function(e,t,a){arguments[4][127][0].apply(a,arguments)},{dup:127,react:"react"}],281:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.cols,a=e.isReady,n=e.showStatus,r=e.users,l="col-xs-12 col-sm-4";return 4===t&&(l+=" col-md-3"),a?o["default"].createElement("div",{className:"users-cards-list ui-ready"},o["default"].createElement("div",{className:"row"},r.map(function(e){return o["default"].createElement("div",{className:l,key:e.id},o["default"].createElement(s["default"],{showStatus:n,user:e}))}))):o["default"].createElement(u["default"],{colClassName:l,cols:t})};var r=e("react"),o=n(r),l=e("./card"),s=n(l),i=e("./preview"),u=n(i)},{"./card":278,"./preview":283,react:"react"}],282:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../avatar"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("div",{className:"panel user-card user-card-preview"},c["default"].createElement("div",{className:"panel-body"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-xs-3 user-card-left"},c["default"].createElement("div",{className:"user-card-small-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"50",size2x:"80"})))),c["default"].createElement("div",{className:"col-xs-9 col-sm-12 user-card-body"},c["default"].createElement("div",{className:"user-card-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"150",size2x:"200"}))),c["default"].createElement("div",{className:"user-card-username"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](60,150)+"px"}}," ")),c["default"].createElement("div",{className:"user-card-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](60,150)+"px"}}," ")),c["default"].createElement("div",{className:"user-card-stats"},c["default"].createElement("ul",{className:"list-unstyled"},c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",{className:"user-stat-divider"}),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," "))))))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":384,"../../avatar":5,react:"react"}],283:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.colClassName,a=e.cols,n=Array.apply(null,{length:a}).map(Number.call,Number);return o["default"].createElement("div",{className:"users-cards-list ui-preview"},o["default"].createElement("div",{className:"row"},n.map(function(e){var a=t;return 0!==e&&(a+=" hidden-xs"),3===e&&(a+=" hidden-sm"),o["default"].createElement("div",{className:a,key:e},o["default"].createElement(s["default"],null))})))};var r=e("react"),o=n(r),l=e("./card"),s=n(l)},{"./card":282,react:"react"}],284:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getEmptyMessage",value:function(){return interpolate(gettext("No users have posted any new messages during last %(days)s days."),{days:this.props.trackedPeriod},!0)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"active-posters-list"},u["default"].createElement("div",{className:"container"},u["default"].createElement("p",{className:"lead"},this.getEmptyMessage())))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],285:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../avatar"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"getClassName",value:function(){return this.props.hiddenOnMobile?"list-group-item hidden-xs hidden-sm":"list-group-item"}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"rank-user-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"50"}))),c["default"].createElement("div",{className:"rank-user"},c["default"].createElement("div",{className:"user-name"},c["default"].createElement("span",{className:"item-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,80)+"px"}}," "))),c["default"].createElement("div",{className:"user-details"},c["default"].createElement("span",{className:"user-status"},c["default"].createElement("span",{className:"status-icon ui-preview-text"}," "),c["default"].createElement("span",{className:"status-label ui-preview-text hidden-xs hidden-sm",style:{width:m["int"](30,50)+"px"}}," ")),c["default"].createElement("span",{className:"rank-name"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,50)+"px"}}," ")),c["default"].createElement("span",{className:"user-title hidden-xs hidden-sm"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,50)+"px"}}," "))),c["default"].createElement("div",{className:"user-compact-stats visible-xs-block"},c["default"].createElement("span",{className:"rank-position"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("span",{className:"rank-posts-counted"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Ranked posts"))))),c["default"].createElement("div",{className:"rank-position hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("div",{className:"rank-posts-counted hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Ranked posts"))),c["default"].createElement("div",{className:"rank-posts-total hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Total posts"))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":384,"../../avatar":5,react:"react"}],286:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("react-router"),f=e("../../avatar"),p=r(f),m=e("../../user-status"),h=r(m),b=e("../../../index"),v=r(b),_=e("../../../utils/random"),g=n(_),y=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getClassName",value:function(){return this.props.rank.css_class?"list-group-item list-group-rank-"+this.props.rank.css_class:"list-group-item"}},{key:"getUserStatus",value:function(){return this.props.user.status?c["default"].createElement(h["default"],{user:this.props.user,status:this.props.user.status},c["default"].createElement(m.StatusIcon,{user:this.props.user,status:this.props.user.status}),c["default"].createElement(m.StatusLabel,{user:this.props.user,status:this.props.user.status,className:"status-label hidden-xs hidden-sm"})):c["default"].createElement("span",{className:"user-status"},c["default"].createElement("span",{className:"status-icon ui-preview-text"}," "),c["default"].createElement("span",{className:"status-label ui-preview-text hidden-xs hidden-sm",style:{width:g["int"](30,50)+"px"}}," "))}},{key:"getRankName",value:function(){if(!this.props.rank.is_tab)return c["default"].createElement("span",{className:"rank-name item-title"},this.props.rank.name);var e=v["default"].get("USERS_LIST_URL")+this.props.rank.slug+"/";return c["default"].createElement(d.Link,{to:e,className:"rank-name item-title"},this.props.rank.name)}},{key:"getUserTitle",value:function(){return this.props.user.title?c["default"].createElement("span",{className:"user-title hidden-xs hidden-sm"},this.props.user.title):null}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"rank-user-avatar"},c["default"].createElement("a",{href:this.props.user.url},c["default"].createElement(p["default"],{user:this.props.user,size:50,size2x:64}))),c["default"].createElement("div",{className:"rank-user"},c["default"].createElement("div",{className:"user-name"},c["default"].createElement("a",{href:this.props.user.url,className:"item-title"},this.props.user.username)),c["default"].createElement("div",{className:"user-details"},this.getUserStatus(),this.getRankName(),this.getUserTitle()),c["default"].createElement("div",{className:"user-compact-stats visible-xs-block"},c["default"].createElement("span",{className:"rank-position"},c["default"].createElement("strong",null,"#",this.props.counter),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("span",{className:"rank-posts-counted"},c["default"].createElement("strong",null,this.props.user.meta.score),c["default"].createElement("small",null,gettext("Ranked posts"))))),c["default"].createElement("div",{className:"rank-position hidden-xs"},c["default"].createElement("strong",null,"#",this.props.counter),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("div",{className:"rank-posts-counted hidden-xs"},c["default"].createElement("strong",null,this.props.user.meta.score),c["default"].createElement("small",null,gettext("Ranked posts"))),c["default"].createElement("div",{className:"rank-posts-total hidden-xs"},c["default"].createElement("strong",null,this.props.user.posts),c["default"].createElement("small",null,gettext("Total posts"))))}}]),t}(c["default"].Component);a["default"]=y},{"../../../index":299,"../../../utils/random":384,"../../avatar":5,"../../user-status":271,react:"react","react-router":"react-router"}],287:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./list-item-preview"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("div",{className:"active-posters-list"},c["default"].createElement("div",{className:"container"},c["default"].createElement("p",{className:"lead ui-preview"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](50,220)+"px"}}," ")),c["default"].createElement("div",{className:"active-posters ui-preview"},c["default"].createElement("ul",{className:"list-group"},[0,1,2].map(function(e){return c["default"].createElement(f["default"],{hiddenOnMobile:e>0,key:e})})))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":384,"./list-item-preview":285,react:"react"}],288:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-item"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getLeadMessage",value:function(){var e=ngettext("%(posters)s most active poster from last %(days)s days.","%(posters)s most active posters from last %(days)s days.",this.props.count);return interpolate(e,{posters:this.props.count,days:this.props.trackedPeriod},!0)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"active-posters-list"},u["default"].createElement("div",{className:"container"},u["default"].createElement("p",{className:"lead"},this.getLeadMessage()),u["default"].createElement("div",{className:"active-posters ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.props.users.map(function(e,t){return u["default"].createElement(d["default"],{user:e,rank:e.rank,counter:t+1,key:e.id})})))))}}]),t}(u["default"].Component);a["default"]=f},{"./list-item":286,react:"react"}],289:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-empty"),d=n(c),f=e("./list-preview"),p=n(f),m=e("./list-ready"),h=n(m),b=e("../../../index"),v=n(b),_=e("../../../reducers/users"),g=e("../../../services/polls"),y=n(g),E=e("../../../services/store"),w=n(E),O=e("../../../services/page-title"),k=n(O),N=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){w["default"].dispatch((0,
-_.hydrate)(e.results)),a.setState({isLoaded:!0,trackedPeriod:e.tracked_period,count:e.count})},v["default"].has("USERS")?a.initWithPreloadedData(v["default"].pop("USERS")):a.initWithoutPreloadedData(),a.startPolling(),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,trackedPeriod:e.tracked_period,count:e.count},w["default"].dispatch((0,_.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(){y["default"].start({poll:"active-posters",url:v["default"].get("USERS_API"),data:{list:"active"},frequency:9e4,update:this.update})}},{key:"componentDidMount",value:function(){k["default"].set({title:this.props.route.extra.name,parent:gettext("Users")})}},{key:"componentWillUnmount",value:function(){y["default"].stop("active-posters")}},{key:"render",value:function(){return this.state.isLoaded?this.state.count>0?u["default"].createElement(h["default"],{users:this.props.users,trackedPeriod:this.state.trackedPeriod,count:this.state.count}):u["default"].createElement(d["default"],{trackedPeriod:this.state.trackedPeriod}):u["default"].createElement(p["default"],null)}}]),t}(u["default"].Component);a["default"]=N},{"../../../index":299,"../../../reducers/users":360,"../../../services/page-title":369,"../../../services/polls":370,"../../../services/store":373,"./list-empty":284,"./list-preview":287,"./list-ready":288,react:"react"}],290:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.baseUrl,a=e.lists;return o["default"].createElement("ul",{className:"nav nav-pills"},a.map(function(e){var a=c(t,e);return o["default"].createElement(i["default"],{path:a,key:a},o["default"].createElement(l.Link,{to:a},e.name))}))};var r=e("react"),o=n(r),l=e("react-router"),s=e("../li"),i=n(s),u=e("../../index"),c=(n(u),function(e,t){var a=e;return a+="rank"===t.component?t.slug:t.component,a+"/"})},{"../../index":299,"../li":55,react:"react","react-router":"react-router"}],291:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../users-list"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement(d["default"],{cols:4,isReady:!1}))}}]),t}(u["default"].Component);a["default"]=f},{"../../users-list":281,react:"react"}],292:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",null,o["default"].createElement(u["default"],{cols:4,isReady:!0,showStatus:!0,users:e.users}),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./pager"),s=n(l),i=e("../../users-list"),u=n(i)},{"../../users-list":281,"./pager":293,react:"react"}],293:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return d["default"].createElement("div",{className:"row row-paginator"},d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(o,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(l,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(s,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(i,e)))}function o(e){return e.isLoaded&&e.first?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl,title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page"))}function l(e){if(e.isLoaded&&e.page>1){var t="";return e.previous&&(t=e.previous+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+t,title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function s(e){if(e.isLoaded&&e.more){var t="";return e.next&&(t=e.next+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+t,title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function i(e){return e.isLoaded&&e.last?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+e.last+"/",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page"))}function u(e){var t=null;return e.more?(t=ngettext("There is %(more)s more member with this role.","There are %(more)s more members with this role.",e.more),t=interpolate(t,{more:e.more},!0)):t=gettext("There are no more members with this role."),d["default"].createElement("p",null,t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return 1===e.pages?null:d["default"].createElement("div",{className:"row row-toolbar"},d["default"].createElement("div",{className:"col-xs-12 text-center visible-xs-block"},d["default"].createElement(u,{more:e.more}),d["default"].createElement("div",{className:"toolbar-vertical-spacer"})),d["default"].createElement("div",{className:"col-md-7"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-4 col-md-5"},d["default"].createElement(r,e)),d["default"].createElement("div",{className:"col-sm-8 col-md-7 hidden-xs"},d["default"].createElement(u,{more:e.more})))))},a.Pager=r,a.FirstPage=o,a.PreviousPage=l,a.NextPage=s,a.LastPage=i,a.More=u;var c=e("react"),d=n(c),f=e("react-router"),p=e("../../../utils/reset-scroll"),m=n(p)},{"../../../utils/reset-scroll":385,react:"react","react-router":"react-router"}],294:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../../page-lead"),f=n(d),p=e("./list"),m=n(p),h=e("./list-loading"),b=n(h),v=e("../../../index"),_=n(v),g=e("../../../reducers/users"),y=e("../../../services/polls"),E=n(y),w=e("../../../services/store"),O=n(w),k=e("../../../services/page-title"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){O["default"].dispatch((0,g.hydrate)(e.results)),e.isLoaded=!0,a.setState(e)},_["default"].has("USERS")?a.initWithPreloadedData(_["default"].pop("USERS")):a.initWithoutPreloadedData(),a.startPolling(e.params.page||1),a}return l(t,e),i(t,[{key:"initWithPreloadedData",value:function(e){this.state=Object.assign(e,{isLoaded:!0}),O["default"].dispatch((0,g.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(e){E["default"].start({poll:"rank-users",url:_["default"].get("USERS_API"),data:{rank:this.props.route.rank.id,page:e},frequency:9e4,update:this.update})}},{key:"componentDidMount",value:function(){N["default"].set({title:this.props.route.rank.name,page:this.props.params.page||null,parent:gettext("Users")})}},{key:"componentWillUnmount",value:function(){E["default"].stop("rank-users")}},{key:"componentWillReceiveProps",value:function(e){this.props.params.page!==e.params.page&&(N["default"].set({title:this.props.route.rank.name,page:e.params.page||null,parent:gettext("Users")}),this.setState({isLoaded:!1}),E["default"].stop("rank-users"),this.startPolling(e.params.page))}},{key:"getClassName",value:function(){return this.props.route.rank.css_class?"rank-users-list rank-users-"+this.props.route.rank.css_class:"rank-users-list"}},{key:"getRankDescription",value:function(){return this.props.route.rank.description?c["default"].createElement("div",{className:"rank-description"},c["default"].createElement(f["default"],{copy:this.props.route.rank.description.html})):null}},{key:"getComponent",value:function(){if(this.state.isLoaded){if(this.state.count>0){var e=_["default"].get("USERS_LIST_URL")+this.props.route.rank.slug+"/";return c["default"].createElement(m["default"],s({baseUrl:e,users:this.props.users},this.state))}return c["default"].createElement("p",{className:"lead"},gettext("There are no users with this rank at the moment."))}return c["default"].createElement(b["default"],null)}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName()},c["default"].createElement("div",{className:"container"},this.getRankDescription(),this.getComponent()))}}]),t}(c["default"].Component);a["default"]=x},{"../../../index":299,"../../../reducers/users":360,"../../../services/page-title":369,"../../../services/polls":370,"../../../services/store":373,"../../page-lead":89,"./list":292,"./list-loading":291,react:"react"}],295:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick,user:e.auth.user,users:e.users}}function i(){var e=[];return O["default"].get("USERS_LISTS").forEach(function(t){"rank"===t.component?(e.push({path:O["default"].get("USERS_LIST_URL")+t.slug+"/:page/",component:(0,f.connect)(s)(g["default"]),rank:t}),e.push({path:O["default"].get("USERS_LIST_URL")+t.slug+"/",component:(0,f.connect)(s)(g["default"]),rank:t})):"active-posters"===t.component&&e.push({path:O["default"].get("USERS_LIST_URL")+t.component+"/",component:(0,f.connect)(s)(v["default"]),extra:{name:t.name}})}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("../dropdown-toggle"),m=(n(p),e("./nav")),h=n(m),b=e("./active-posters/root"),v=n(b),_=e("./rank/root"),g=n(_),y=e("../with-dropdown"),E=n(y),w=e("../../index"),O=n(w),k=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-users-lists"},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:"page-header"},d["default"].createElement("div",{className:"container"},d["default"].createElement("h1",null,gettext("Users"))),d["default"].createElement("div",{className:"page-tabs"},d["default"].createElement("div",{className:"container"},d["default"].createElement(h["default"],{lists:O["default"].get("USERS_LISTS"),baseUrl:O["default"].get("USERS_LIST_URL")}))))),this.props.children)}}]),t}(E["default"]);a["default"]=k},{"../../index":299,"../dropdown-toggle":26,"../with-dropdown":296,"./active-posters/root":289,"./nav":290,"./rank/root":294,react:"react","react-redux":"react-redux"}],296:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.toggleNav=function(){a.setState({dropdown:!a.state.dropdown})},a.hideNav=function(){a.setState({dropdown:!1})},a.state={dropdown:!1},a}return l(t,e),s(t,[{key:"getCompactNavClassName",value:function(){return this.state.dropdown?"compact-nav open":"compact-nav"}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],297:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.toggle=function(){n.props.onChange({target:{value:!n.props.value}})},l=a,o(n,l)}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.value?"btn btn-yes-no btn-yes-no-on":"btn btn-yes-no btn-yes-no-off"}},{key:"getIcon",value:function(){return this.props.value?this.props.iconOn||"check_box":this.props.iconOff||"check_box_outline_blank"}},{key:"getLabel",value:function(){return this.props.value?this.props.labelOn||gettext("yes"):this.props.labelOff||gettext("no")}},{key:"render",value:function(){return u["default"].createElement("button",{type:"button",onClick:this.toggle,className:this.getClassName(),id:this.props.id||null,"aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},u["default"].createElement("span",{className:"material-icon"},this.getIcon()),u["default"].createElement("span",{className:"btn-text"},this.getLabel()))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],298:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../reducers/profile-details"),d=e("../services/ajax"),f=n(d),p=e("../services/snackbar"),m=n(p),h=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this.props,t=e.data,a=e.dispatch,n=e.user;t&&t.id===n.id||f["default"].get(this.props.user.api.details).then(function(e){a((0,c.load)(e))},function(e){m["default"].apiError(e)})}},{key:"render",value:function(){return this.props.children}}]),t}(u["default"].Component);a["default"]=h},{"../reducers/profile-details":351,"../services/ajax":361,"../services/snackbar":372,react:"react"}],299:[function(e,t,a){(function(t){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Misago=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("./utils/ordered-list"),s=n(l),i=a.Misago=function(){function e(){r(this,e),this._initializers=[],this._context={}}return o(e,[{key:"addInitializer",value:function(e){this._initializers.push({key:e.name,item:e.initializer,after:e.after,before:e.before})}},{key:"init",value:function(e){var t=this;this._context=e;var a=new s["default"](this._initializers).orderedValues();a.forEach(function(e){e(t)})}},{key:"has",value:function(e){return!!this._context[e]}},{key:"get",value:function(e,t){return this.has(e)?this._context[e]:t||void 0}},{key:"pop",value:function(e){if(this.has(e)){var t=this._context[e];return this._context[e]=null,t}}}]),e}(),u=new i;t.misago=u,a["default"]=u}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./utils/ordered-list":383}],300:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(l["default"].get("CSRF_COOKIE_NAME"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s);l["default"].addInitializer({name:"ajax",initializer:r})},{"../index":299,"../services/ajax":361}],301:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.get("isAuthenticated")&&window.setInterval(function(){u["default"].get(e.get("AUTH_API")).then(function(e){p["default"].dispatch((0,s.patch)(e))},function(e){d["default"].apiError(e)})},1e3*m)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../reducers/auth"),i=e("../services/ajax"),u=n(i),c=e("../services/snackbar"),d=n(c),f=e("../services/store"),p=n(f),m=45;l["default"].addInitializer({name:"auth-sync",initializer:r,after:"auth"})},{"../index":299,"../reducers/auth":346,"../services/ajax":361,"../services/snackbar":372,"../services/store":373}],302:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(f["default"],m["default"],c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/auth"),i=n(s),u=e("../services/modal"),c=n(u),d=e("../services/store"),f=n(d),p=e("../services/local-storage"),m=n(p);l["default"].addInitializer({name:"auth",initializer:r,after:"store"})},{"../index":299,"../services/auth":362,"../services/local-storage":365,"../services/modal":367,"../services/store":373}],303:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){c["default"].init(e,i["default"],f["default"],m["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/captcha"),c=n(u),d=e("../services/include"),f=n(d),p=e("../services/snackbar"),m=n(p);l["default"].addInitializer({name:"captcha",initializer:r})},{"../index":299,"../services/ajax":361,"../services/captcha":363,"../services/include":364,"../services/snackbar":372}],304:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,d["default"])((0,o.connect)(i.select)(u["default"]),"auth-message-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/auth-message"),u=n(i),c=e("../../utils/mount-component"),d=n(c);s["default"].addInitializer({name:"component:auth-message",initializer:r,after:"store"})},{"../../components/auth-message":4,"../../index":299,"../../utils/mount-component":382,"react-redux":"react-redux"}],305:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("BAN_MESSAGE")&&(0,i["default"])(e.get("BAN_MESSAGE"),!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../utils/banned-page"),i=n(s);l["default"].addInitializer({name:"component:banmed-page",initializer:r,after:"store"})},{"../../index":299,"../../utils/banned-page":375}],306:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("categories-mount")&&(0,d["default"])((0,o.connect)(l.select)(s["default"]),"categories-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../components/categories"),s=n(l),i=e("../../index"),u=n(i),c=e("../../utils/mount-component"),d=n(c);u["default"].addInitializer({name:"component:categories",initializer:r,after:"store"})},{"../../components/categories":19,"../../index":299,"../../utils/mount-component":382,"react-redux":"react-redux"}],307:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("USER_OPTIONS")&&(0,c["default"])({root:i["default"].get("USERCP_URL"),component:l["default"],paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/options/root"),l=n(o),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:options",initializer:r,after:"store"})},{"../../components/options/root":84,"../../index":299,"../../utils/routed-component":386}],308:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("PROFILE")&&e.has("PROFILE_PAGES")&&(0,d["default"])({root:u["default"].get("PROFILE").url,component:(0,o.connect)(l.select)(s["default"]),paths:(0,l.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../components/profile/root"),s=n(l),i=e("../../index"),u=n(i),c=e("../../utils/routed-component"),d=n(c);u["default"].addInitializer({name:"component:profile",initializer:r,after:"reducer:profile-hydrate"})},{"../../components/profile/root":192,"../../index":299,"../../utils/routed-component":386,"react-redux":"react-redux"}],309:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("request-activation-link-mount")&&(0,c["default"])(i["default"],"request-activation-link-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../components/request-activation-link"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:request-activation-link",initializer:r,after:"store"})},{"../../components/request-activation-link":197,"../../index":299,"../../utils/mount-component":382}],310:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("request-password-reset-mount")&&(0,c["default"])(i["default"],"request-password-reset-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../components/request-password-reset"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:request-password-reset",initializer:r,after:"store"})},{"../../components/request-password-reset":198,"../../index":299,"../../utils/mount-component":382}],311:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("reset-password-form-mount")&&(0,c["default"])(i["default"],"reset-password-form-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../.."),l=n(o),s=e("../../components/reset-password-form"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:reset-password-form",initializer:r,after:"store"})},{"../..":299,"../../components/reset-password-form":199,"../../utils/mount-component":382}],312:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){"misago:search"===e.get("CURRENT_LINK")&&(0,c["default"])({paths:(0,l["default"])(i["default"].get("SEARCH_PROVIDERS"))})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/search"),l=n(o),s=e("../.."),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:search",initializer:r,after:"store"})},{"../..":299,"../../components/search":201,"../../utils/routed-component":386}],313:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,c["default"])((0,o.connect)(i.select)(i.Snackbar),"snackbar-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/snackbar"),u=e("../../utils/mount-component"),c=n(u);s["default"].addInitializer({name:"component:snackbar",initializer:r,after:"snackbar"})},{"../../components/snackbar":209,"../../index":299,"../../utils/mount-component":382,"react-redux":"react-redux"}],314:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if("social:complete"===e.get("CURRENT_LINK")){var t=e.get("SOCIAL_AUTH");(0,f["default"])(l["default"].createElement(i["default"],t),"page-mount")}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react"),l=n(o),s=e("../../components/social-auth"),i=n(s),u=e("../.."),c=n(u),d=e("../../utils/mount-component"),f=n(d);c["default"].addInitializer({name:"component:social-auth",initializer:r,after:"store"})},{"../..":299,"../../components/social-auth":212,"../../utils/mount-component":382,react:"react"}],315:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("THREAD")&&e.has("POSTS")&&(0,u["default"])({paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/thread/root"),l=e("../../index"),s=n(l),i=e("../../utils/routed-component"),u=n(i);s["default"].addInitializer({name:"component:thread",initializer:r,after:"store"})},{"../../components/thread/root":230,"../../index":299,"../../utils/routed-component":386}],316:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("THREADS")&&e.has("CATEGORIES")&&(0,c["default"])({paths:(0,l.paths)(e.get("user"),o(e))})}function o(e){var t=e.get("CURRENT_LINK");return t.substr(0,d.length)===d?{api:e.get("PRIVATE_THREADS_API"),startThread:{mode:"START_PRIVATE",submit:i["default"].get("PRIVATE_THREADS_API")},title:gettext("Private threads"),pageLead:gettext("Private threads are threads which only those that started them and those they have invited may see and participate in."),emptyMessage:gettext("You aren't participating in any private threads.")}:{api:e.get("THREADS_API")}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r,a.getListOptions=o;var l=e("../../components/threads/root"),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u),d="misago:private-threads";i["default"].addInitializer({name:"component:threads",initializer:r,after:"store"})},{"../../components/threads/root":264,"../../index":299,"../../utils/routed-component":386}],317:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,c["default"])((0,o.connect)(i.select)(i.UserMenu),"user-menu-mount"),(0,c["default"])((0,o.connect)(i.select)(i.CompactUserMenu),"user-menu-compact-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/user-menu/root"),u=e("../../utils/mount-component"),c=n(u);s["default"].addInitializer({name:"component:user-menu",initializer:r,after:"store"})},{"../../components/user-menu/root":269,"../../index":299,"../../utils/mount-component":382,
-"react-redux":"react-redux"}],318:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("USERS_LISTS")&&(0,c["default"])({root:i["default"].get("USERS_LIST_URL"),component:l["default"],paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/users/root"),l=n(o),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:users",initializer:r,after:"store"})},{"../../components/users/root":295,"../../index":299,"../../utils/routed-component":386}],319:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){i["default"].init(e.get("STATIC_URL"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/include"),i=n(s);l["default"].addInitializer({name:"include",initializer:r})},{"../index":299,"../services/include":364}],320:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init("misago_")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/local-storage"),i=n(s);l["default"].addInitializer({name:"local-storage",initializer:r})},{"../index":299,"../services/local-storage":365}],321:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=document.getElementById("mobile-navbar-dropdown-mount");e&&i["default"].init(e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/mobile-navbar-dropdown"),i=n(s);l["default"].addInitializer({name:"dropdown",initializer:r,before:"store"})},{"../index":299,"../services/mobile-navbar-dropdown":366}],322:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=document.getElementById("modal-mount");e&&i["default"].init(e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/modal"),i=n(s);l["default"].addInitializer({name:"modal",initializer:r,before:"store"})},{"../index":299,"../services/modal":367}],323:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){l["default"].locale($("html").attr("lang"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("moment"),l=n(o),s=e("../index"),i=n(s);i["default"].addInitializer({name:"moment",initializer:r})},{"../index":299,moment:"moment"}],324:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){i["default"].init(e.get("SETTINGS").forum_index_title,e.get("SETTINGS").forum_name)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/page-title"),i=n(s);l["default"].addInitializer({name:"page-title",initializer:r})},{"../index":299,"../services/page-title":369}],325:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){f["default"].init(i["default"],c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/snackbar"),c=n(u),d=e("../services/polls"),f=n(d);l["default"].addInitializer({name:"polls",initializer:r})},{"../index":299,"../services/ajax":361,"../services/polls":370,"../services/snackbar":372}],326:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].init(i["default"],f["default"],document.getElementById("posting-placeholder"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/posting"),c=n(u),d=e("../services/snackbar"),f=n(d);l["default"].addInitializer({name:"posting",initializer:r})},{"../index":299,"../services/ajax":361,"../services/posting":371,"../services/snackbar":372}],327:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){c["default"].addReducer("auth",i["default"],Object.assign({isAuthenticated:e.get("isAuthenticated"),isAnonymous:!e.get("isAuthenticated"),user:e.get("user")},s.initialState))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/auth"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:auth",initializer:r,before:"store"})},{"../../index":299,"../../reducers/auth":346,"../../services/store":373}],328:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;l["default"].has("THREAD")&&(e=l["default"].get("THREAD").participants),c["default"].addReducer("participants",i["default"],e||[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/participants"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:participants",initializer:r,before:"store"})},{"../../index":299,"../../reducers/participants":347,"../../services/store":373}],329:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("THREAD")&&l["default"].get("THREAD").poll?(0,s.hydrate)(l["default"].get("THREAD").poll):{isBusy:!1},c["default"].addReducer("poll",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/poll"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:poll",initializer:r,before:"store"})},{"../../index":299,"../../reducers/poll":348,"../../services/store":373}],330:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("POSTS")?(0,s.hydrate)(l["default"].get("POSTS")):{isLoaded:!1,isBusy:!1},c["default"].addReducer("posts",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/posts"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:posts",initializer:r,before:"store"})},{"../../index":299,"../../reducers/posts":350,"../../services/store":373}],331:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;l["default"].has("PROFILE_DETAILS")&&(e=l["default"].get("PROFILE_DETAILS")),c["default"].addReducer("profile-details",i["default"],e||{})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile-details"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:profile-details",initializer:r,before:"store"})},{"../../index":299,"../../reducers/profile-details":351,"../../services/store":373}],332:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){l["default"].has("PROFILE")&&u["default"].dispatch((0,s.hydrate)(l["default"].get("PROFILE")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile"),i=e("../../services/store"),u=n(i);l["default"].addInitializer({name:"reducer:profile-hydrate",initializer:r,after:"store"})},{"../../index":299,"../../reducers/profile":352,"../../services/store":373}],333:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("profile",i["default"],{})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:profile",initializer:r,before:"store"})},{"../../index":299,"../../reducers/profile":352,"../../services/store":373}],334:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("search",i["default"],Object.assign({},s.initialState,{providers:l["default"].get("SEARCH_PROVIDERS")||[],query:l["default"].get("SEARCH_QUERY")||""}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../.."),l=n(o),s=e("../../reducers/search"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:search",initializer:r,before:"store"})},{"../..":299,"../../reducers/search":353,"../../services/store":373}],335:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("selection",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/selection"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:selection",initializer:r,before:"store"})},{"../../index":299,"../../reducers/selection":354,"../../services/store":373}],336:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("snackbar",i["default"],s.initialState)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/snackbar"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:snackbar",initializer:r,before:"store"})},{"../../index":299,"../../reducers/snackbar":355,"../../services/store":373}],337:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("THREAD")?(0,s.hydrate)(l["default"].get("THREAD")):{isBusy:!1},c["default"].addReducer("thread",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/thread"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:thread",initializer:r,before:"store"})},{"../../index":299,"../../reducers/thread":356,"../../services/store":373}],338:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("threads",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/threads"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:threads",initializer:r,before:"store"})},{"../../index":299,"../../reducers/threads":357,"../../services/store":373}],339:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("tick",i["default"],s.initialState)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/tick"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:tick",initializer:r,before:"store"})},{"../../index":299,"../../reducers/tick":358,"../../services/store":373}],340:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("username-history",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/username-history"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:username-history",initializer:r,before:"store"})},{"../../index":299,"../../reducers/username-history":359,"../../services/store":373}],341:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("users",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/users"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:users",initializer:r,before:"store"})},{"../../index":299,"../../reducers/users":360,"../../services/store":373}],342:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/snackbar"),i=n(s),u=e("../services/store"),c=n(u);l["default"].addInitializer({name:"snackbar",initializer:r,after:"store"})},{"../index":299,"../services/snackbar":372,"../services/store":373}],343:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init()}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/store"),i=n(s);l["default"].addInitializer({name:"store",initializer:r,before:"_end"})},{"../index":299,"../services/store":373}],344:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){window.setInterval(function(){u["default"].dispatch((0,s.doTick)())},c)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../reducers/tick"),i=e("../services/store"),u=n(i),c=5e4;l["default"].addInitializer({name:"tick-start",initializer:r,after:"store"})},{"../index":299,"../reducers/tick":358,"../services/store":373}],345:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].init(i["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/include"),i=n(s),u=e("../services/zxcvbn"),c=n(u);l["default"].addInitializer({name:"zxcvbn",initializer:r})},{"../index":299,"../services/include":364,"../services/zxcvbn":374}],346:[function(e,t,a){"use strict";function n(e){return{type:u,patch:e}}function r(e){return{type:c,user:e}}function o(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return{type:d,soft:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:i,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case u:var a=Object.assign({},e);return a.user=Object.assign({},e.user,t.patch),a;case c:return Object.assign({},e,{signedIn:t.user});case d:return Object.assign({},e,{isAuthenticated:!1,isAnonymous:!0,signedOut:!t.soft});case s.UPDATE_AVATAR:if(e.isAuthenticated&&e.user.id===t.userId){var n=Object.assign({},e);return n.user=Object.assign({},e.user,{avatars:t.avatars}),n}return e;case s.UPDATE_USERNAME:if(e.isAuthenticated&&e.user.id===t.userId){var r=Object.assign({},e);return r.user=Object.assign({},e.user,{username:t.username,slug:t.slug}),r}return e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.SIGN_OUT=a.SIGN_IN=a.PATCH_USER=a.initialState=void 0,a.patch=n,a.signIn=r,a.signOut=o,a["default"]=l;var s=e("./users"),i=a.initialState={signedIn:!1,signedOut:!1},u=a.PATCH_USER="PATCH_USER",c=a.SIGN_IN="SIGN_IN",d=a.SIGN_OUT="SIGN_OUT"},{"./users":360}],347:[function(e,t,a){"use strict";function n(e){return{type:o,state:e}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case o:return t.state;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.replace=n,a["default"]=r;var o=a.REPLACE_PARTICIPANTS="REPLACE_PARTICIPANTS"},{}],348:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=!1;for(var a in e.choices){var n=e.choices[a];if(n.selected){t=!0;break}}return Object.assign({},e,{posted_on:(0,f["default"])(e.posted_on),hasSelectedChoices:t,endsOn:e.length?(0,f["default"])(e.posted_on).add(e.length,"days"):null,isBusy:!1})}function o(){return{type:p}}function l(){return{type:m}}function s(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:b,state:t?e:r(e)}}function i(e){return{type:v,data:e}}function u(){return{type:h}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case p:return Object.assign({},e,{isBusy:!0});case m:return Object.assign({},e,{isBusy:!1});case h:return{isBusy:!1};case b:return t.state;case v:return Object.assign({},e,t.data);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_POLL=a.REPLACE_POLL=a.REMOVE_POLL=a.RELEASE_POLL=a.BUSY_POLL=void 0,a.hydrate=r,a.busy=o,a.release=l,a.replace=s,a.update=i,a.remove=u,a["default"]=c;var d=e("moment"),f=n(d),p=a.BUSY_POLL="BUSY_POLL",m=a.RELEASE_POLL="RELEASE_POLL",h=a.REMOVE_POLL="REMOVE_POLL",b=a.REPLACE_POLL="REPLACE_POLL",v=a.UPDATE_POLL="UPDATE_POLL"},{moment:"moment"}],349:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{posted_on:(0,u["default"])(e.posted_on),updated_on:(0,u["default"])(e.updated_on),hidden_on:(0,u["default"])(e.hidden_on),attachments:e.attachments?e.attachments.map(o):null,poster:e.poster?(0,c.hydrateUser)(e.poster):null,isSelected:!1,isBusy:!1,isDeleted:!1})}function o(e){return Object.assign({},e,{uploaded_on:(0,u["default"])(e.uploaded_on)})}function l(e,t){return{type:d,post:e,patch:t}}function s(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case d:return e.id==t.post.id?Object.assign({},e,t.patch):e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PATCH_POST=void 0,a.hydrate=r,a.hydrateAttachment=o,a.patch=l,a["default"]=s;var i=e("moment"),u=n(i),c=e("./users"),d=a.PATCH_POST="PATCH_POST"},{"./users":360,moment:"moment"}],350:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:b,post:e}}function o(e){return{type:v,post:e}}function l(){return{type:_}}function s(e){return Object.assign({},e,{results:e.results.map(p.hydrate),isLoaded:!0,isBusy:!1,isSelected:!1})}function i(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:g,state:t?e:s(e)}}function u(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:h,state:t?e:s(e)}}function c(){return{type:y}}function d(e){return{type:E,update:e}}function f(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case b:var a=e.results.map(function(e){return e.id==t.post.id?Object.assign({},e,{isSelected:!0}):e});return Object.assign({},e,{results:a});case v:var n=e.results.map(function(e){return e.id==t.post.id?Object.assign({},e,{isSelected:!1}):e});return Object.assign({},e,{results:n});case _:var r=e.results.map(function(e){return Object.assign({},e,{isSelected:!1})});return Object.assign({},e,{results:r});case h:var o=e.results.slice(),l=e.results.map(function(e){return e.id});return t.state.results.map(function(e){l.indexOf(e.id)===-1&&o.push(e)}),Object.assign({},t.state,{results:o});case g:return t.state;case y:return Object.assign({},e,{isLoaded:!1});case E:return Object.assign({},e,t.update);case p.PATCH_POST:var s=e.results.map(function(e){return(0,m["default"])(e,t)});return Object.assign({},e,{results:s});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_POSTS=a.UNLOAD_POSTS=a.LOAD_POSTS=a.DESELECT_POSTS=a.DESELECT_POST=a.SELECT_POST=a.APPEND_POSTS=void 0,a.select=r,a.deselect=o,a.deselectAll=l,a.hydrate=s,a.load=i,a.append=u,a.unload=c,a.update=d,a["default"]=f;var p=e("./post"),m=n(p),h=a.APPEND_POSTS="APPEND_POSTS",b=a.SELECT_POST="SELECT_POST",v=a.DESELECT_POST="DESELECT_POST",_=a.DESELECT_POSTS="DESELECT_POSTS",g=a.LOAD_POSTS="LOAD_POSTS",y=a.UNLOAD_POSTS="UNLOAD_POSTS",E=a.UPDATE_POSTS="UPDATE_POSTS"},{"./post":349}],351:[function(e,t,a){"use strict";function n(e){return{type:o,newState:e}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case o:return t.newState;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.load=n,a["default"]=r;var o=a.LOAD_DETAILS="LOAD_DETAILS"},{}],352:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:c,profile:e}}function o(e){return{type:d,patch:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case c:return Object.assign({},t.profile,{joined_on:(0,i["default"])(t.profile.joined_on),status:(0,u.hydrateStatus)(t.profile.status)});case d:return Object.assign({},e,t.patch);case u.UPDATE_AVATAR:return e.id===t.userId?Object.assign({},e,{avatars:t.avatars}):e;case u.UPDATE_USERNAME:return e.id===t.userId?Object.assign({},e,{username:t.username,slug:t.slug}):e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PATCH_PROFILE=a.HYDRATE_PROFILE=void 0,a.hydrate=r,a.patch=o,a["default"]=l;var s=e("moment"),i=n(s),u=e("./users"),c=a.HYDRATE_PROFILE="HYDRATE_PROFILE",d=a.PATCH_PROFILE="PATCH_PROFILE"},{"./users":360,moment:"moment"}],353:[function(e,t,a){"use strict";function n(e){return{type:s,state:{isLoading:!1,providers:e}}}function r(e){return{type:i,update:e}}function o(e){return{type:u,provider:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case s:return t.state;case i:return Object.assign({},e,t.update);case u:return Object.assign({},e,{providers:e.providers.map(function(e){return e.id===t.provider.id?t.provider:e})});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.replace=n,a.update=r,a.updateProvider=o,a["default"]=l;var s=a.REPLACE_SEARCH="REPLACE_SEARCH",i=a.UPDATE_SEARCH="UPDATE_SEARCH",u=a.UPDATE_SEARCH_PROVIDER="UPDATE_SEARCH_PROVIDER";a.initialState={isLoading:!1,query:"",providers:[]}},{}],354:[function(e,t,a){"use strict";function n(e){return{type:i,items:e}}function r(){return{type:u}}function o(e){return{type:c,item:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case i:return t.items;case u:return[];case c:return(0,s.toggle)(e,t.item);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.SELECT_ITEM=a.SELECT_NONE=a.SELECT_ALL=void 0,a.all=n,a.none=r,a.item=o,a["default"]=l;var s=e("../utils/sets"),i=a.SELECT_ALL="SELECT_ALL",u=a.SELECT_NONE="SELECT_NONE",c=a.SELECT_ITEM="SELECT_ITEM"},{"../utils/sets":387}],355:[function(e,t,a){"use strict";function n(e,t){return{type:s,message:e,messageType:t}}function r(){return{type:i}}function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:l,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return t.type===s?{type:t.messageType,message:t.message,isVisible:!0}:t.type===i?Object.assign({},e,{isVisible:!1}):e}Object.defineProperty(a,"__esModule",{value:!0}),a.showSnackbar=n,a.hideSnackbar=r,a["default"]=o;var l=a.initialState={type:"info",message:"",isVisible:!1},s=a.SHOW_SNACKBAR="SHOW_SNACKBAR",i=a.HIDE_SNACKBAR="HIDE_SNACKBAR"},{}],356:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{started_on:(0,f["default"])(e.started_on),last_post_on:(0,f["default"])(e.last_post_on),best_answer_marked_on:e.best_answer_marked_on?(0,f["default"])(e.best_answer_marked_on):null,isBusy:!1})}function o(){return{type:m}}function l(){return{type:h}}function s(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:b,state:t?e:r(e)}}function i(e){return{type:v,data:e}}function u(e){return{type:_,data:e}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case m:return Object.assign({},e,{isBusy:!0});case h:return Object.assign({},e,{isBusy:!1});case p.REMOVE_POLL:return Object.assign({},e,{poll:null});case p.REPLACE_POLL:return Object.assign({},e,{poll:t.state});case b:return t.state;case v:return Object.assign({},e,t.data);case _:var a=Object.assign({},e.acl,t.data);return Object.assign({},e,{acl:a});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_THREAD_ACL=a.UPDATE_THREAD=a.REPLACE_THREAD=a.RELEASE_THREAD=a.BUSY_THREAD=void 0,a.hydrate=r,a.busy=o,a.release=l,a.replace=s,a.update=i,a.updateAcl=u,a["default"]=c;var d=e("moment"),f=n(d),p=e("./poll"),m=a.BUSY_THREAD="BUSY_THREAD",h=a.RELEASE_THREAD="RELEASE_THREAD",b=a.REPLACE_THREAD="REPLACE_THREAD",v=a.UPDATE_THREAD="UPDATE_THREAD",_=a.UPDATE_THREAD_ACL="UPDATE_THREAD_ACL"},{"./poll":348,moment:"moment"}],357:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){return{type:v,items:e,sorting:t}}function o(e){return{type:_,thread:e}}function l(e,t){return{type:g,category:e,categoriesMap:t}}function s(e){return{type:y,items:e}}function i(e,t){var a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;return{type:E,thread:e,patch:t,sorting:a}}function u(e){return{type:w,sorting:e}}function c(e){var t=[];return O.forEach(function(a){e[a]&&t.push(a)}),t}function d(e){return Object.assign({},e,{started_on:(0,m["default"])(e.started_on),last_post_on:(0,m["default"])(e.last_post_on),moderation:c(e.acl)})}function f(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case v:var a=(0,b["default"])(t.items.map(d),e);return a.sort(t.sorting);case _:return e.filter(function(e){return e.id!==t.thread.id});case g:return e.filter(function(e){var a=t.categoriesMap[e.category];return a.lft>=t.category.lft&&a.rght<=t.category.rght||2==e.weight});case y:return t.items.map(d);case E:var n=e.map(function(e){return e.id===t.thread.id?Object.assign({},e,t.patch):e});return t.sorting?n.sort(t.sorting):n;case w:return e.sort(t.sorting);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.MODERATION_PERMISSIONS=a.SORT_THREADS=a.PATCH_THREAD=a.HYDRATE_THREADS=a.FILTER_THREADS=a.DELETE_THREAD=a.APPEND_THREADS=void 0,a.append=r,a.deleteThread=o,a.filterThreads=l,a.hydrate=s,a.patch=i,a.sort=u,a.getThreadModerationOptions=c,a.hydrateThread=d,a["default"]=f;var p=e("moment"),m=n(p),h=e("../utils/concat-unique"),b=n(h),v=a.APPEND_THREADS="APPEND_THREADS",_=a.DELETE_THREAD="DELETE_THREAD",g=a.FILTER_THREADS="FILTER_THREADS",y=a.HYDRATE_THREADS="HYDRATE_THREADS",E=a.PATCH_THREAD="PATCH_THREAD",w=a.SORT_THREADS="SORT_THREADS",O=a.MODERATION_PERMISSIONS=["can_announce","can_approve","can_close","can_hide","can_move","can_merge","can_pin","can_review"]},{"../utils/concat-unique":377,moment:"moment"}],358:[function(e,t,a){"use strict";function n(){return{type:l}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return t.type===l?Object.assign({},e,{tick:e.tick+1}):e}Object.defineProperty(a,"__esModule",{value:!0}),a.doTick=n,a["default"]=r;var o=a.initialState={tick:0},l=a.TICK="TICK"},{}],359:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return{type:m,change:e,user:t,changedBy:a}}function o(e){return{type:h,items:e}}function l(e){return{type:b,items:e}}function s(e){return Object.assign({},e,{changed_on:(0,c["default"])(e.changed_on)})}function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case m:var a=e.slice();return a.unshift({id:Math.floor(Date.now()/1e3),changed_by:t.changedBy,changed_by_username:t.changedBy.username,changed_on:(0,c["default"])(),new_username:t.change.username,old_username:t.user.username}),a;case h:return(0,p["default"])(e,t.items.map(s));case b:return t.items.map(s);case d.UPDATE_AVATAR:return e.map(function(e){return e=Object.assign({},e),e.changed_by&&e.changed_by.id===t.userId&&(e.changed_by=Object.assign({},e.changed_by,{avatars:t.avatars})),e});case d.UPDATE_USERNAME:return e.map(function(e){return e=Object.assign({},e),e.changed_by&&e.changed_by.id===t.userId&&(e.changed_by=Object.assign({},e.changed_by,{username:t.username,slug:t.slug})),Object.assign({},e)});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.HYDRATE_HISTORY=a.APPEND_HISTORY=a.ADD_NAME_CHANGE=void 0,a.addNameChange=r,a.append=o,a.hydrate=l,a.hydrateNamechange=s,a["default"]=i;var u=e("moment"),c=n(u),d=e("./users"),f=e("../utils/concat-unique"),p=n(f),m=a.ADD_NAME_CHANGE="ADD_NAME_CHANGE",h=a.APPEND_HISTORY="APPEND_HISTORY",b=a.HYDRATE_HISTORY="HYDRATE_HISTORY"},{"../utils/concat-unique":377,"./users":360,moment:"moment"}],360:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:h,items:e}}function o(e){return{type:b,items:e}}function l(e){return e?Object.assign({},e,{last_click:e.last_click?(0,f["default"])(e.last_click):null,banned_until:e.banned_until?(0,f["default"])(e.banned_until):null}):null}function s(e){return Object.assign({},e,{joined_on:(0,f["default"])(e.joined_on),status:l(e.status)})}function i(e,t){return{type:v,userId:e.id,avatars:t}}function u(e,t,a){return{type:_,userId:e.id,username:t,slug:a}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case h:return(0,m["default"])(e,t.items.map(s));case b:return t.items.map(s);case v:return e.map(function(e){return e=Object.assign({},e),e.id===t.userId&&(e.avatars=t.avatars),e});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_USERNAME=a.UPDATE_AVATAR=a.HYDRATE_USERS=a.APPEND_USERS=void 0,a.append=r,a.hydrate=o,a.hydrateStatus=l,a.hydrateUser=s,a.updateAvatar=i,a.updateUsername=u,a["default"]=c;var d=e("moment"),f=n(d),p=e("../utils/concat-unique"),m=n(p),h=a.APPEND_USERS="APPEND_USERS",b=a.HYDRATE_USERS="HYDRATE_USERS",v=a.UPDATE_AVATAR="UPDATE_AVATAR",_=a.UPDATE_USERNAME="UPDATE_USERNAME"},{"../utils/concat-unique":377,moment:"moment"}],361:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Ajax=function(){function e(){n(this,e),this._cookieName=null,this._csrfToken=null,this._locks={}}return r(e,[{key:"init",value:function(e){this._cookieName=e}},{key:"getCsrfToken",value:function(){if(document.cookie.indexOf(this._cookieName)!==-1){var e=new RegExp(this._cookieName+"=([^;]*)"),t=document.cookie.match(e)[0];return t?t.split("=")[1]:null}return null}},{key:"request",value:function(e,t,a){var n=this;return new Promise(function(r,o){var l={url:t,method:e,headers:{"X-CSRFToken":n.getCsrfToken()},data:a?JSON.stringify(a):null,contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){r(e)},error:function(e){var t=e.responseJSON||{};t.status=e.status,0===t.status&&(t.detail=gettext("Lost connection with application.")),404===t.status&&(t.detail&&"NOT FOUND"!==t.detail||(t.detail=gettext("Action link is invalid."))),500!==t.status||t.detail||(t.detail=gettext("Unknown error has occured.")),t.statusText=e.statusText,o(t)}};$.ajax(l)})}},{key:"get",value:function(e,t,a){if(t&&(e+="?"+$.param(t)),a){var n=this;return this._locks[a]&&(this._locks[a].url=e),this._locks[a]&&this._locks[a].waiter?{then:function(){}}:this._locks[a]&&this._locks[a].wait?(this._locks[a].waiter=!0,new Promise(function(t,r){var o=function l(e){n._locks[a].wait?window.setTimeout(function(){l(e)},300):n._locks[a].url!==e?l(n._locks[a].url):(n._locks[a].waiter=!1,
-n.request("GET",n._locks[a].url).then(function(r){n._locks[a].url===e?t(r):(n._locks[a].waiter=!0,l(n._locks[a].url))},function(t){n._locks[a].url===e?r(t):(n._locks[a].waiter=!0,l(n._locks[a].url))}))};window.setTimeout(function(){o(e)},300)})):(this._locks[a]={url:e,wait:!0,waiter:!1},new Promise(function(t,r){n.request("GET",e).then(function(r){n._locks[a].wait=!1,n._locks[a].url===e&&t(r)},function(t){n._locks[a].wait=!1,n._locks[a].url===e&&r(t)})}))}return this.request("GET",e)}},{key:"post",value:function(e,t){return this.request("POST",e,t)}},{key:"patch",value:function(e,t){return this.request("PATCH",e,t)}},{key:"put",value:function(e,t){return this.request("PUT",e,t)}},{key:"delete",value:function(e,t){return this.request("DELETE",e,t)}},{key:"upload",value:function(e,t,a){var n=this;return new Promise(function(r,o){var l={url:e,method:"POST",headers:{"X-CSRFToken":n.getCsrfToken()},data:t,contentType:!1,processData:!1,xhr:function s(){var s=new window.XMLHttpRequest;return s.upload.addEventListener("progress",function(e){e.lengthComputable&&a(Math.round(e.loaded/e.total*100))},!1),s},success:function(e){r(e)},error:function(e){var t=e.responseJSON||{};t.status=e.status,0===t.status&&(t.detail=gettext("Lost connection with application.")),413!==t.status||t.detail||(t.detail=gettext("Upload was rejected by server as too large.")),404===t.status&&(t.detail&&"NOT FOUND"!==t.detail||(t.detail=gettext("Action link is invalid."))),500!==t.status||t.detail||(t.detail=gettext("Unknown error has occured.")),t.statusText=e.statusText,o(t)}};$.ajax(l)})}}]),e}();a["default"]=new o},{}],362:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Auth=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("../reducers/auth"),l=a.Auth=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t,a){this._store=e,this._local=t,this._modal=a,this.syncSession(),this.watchState()}},{key:"syncSession",value:function(){var e=this._store.getState().auth;e.isAuthenticated?this._local.set("auth",{isAuthenticated:!0,username:e.user.username}):this._local.set("auth",{isAuthenticated:!1})}},{key:"watchState",value:function(){var e=this,t=this._store.getState().auth;this._local.watch("auth",function(a){a.isAuthenticated?e._store.dispatch((0,o.signIn)({username:a.username})):t.isAuthenticated&&e._store.dispatch((0,o.signOut)())}),this._modal.hide()}},{key:"signIn",value:function(e){this._store.dispatch((0,o.signIn)(e)),this._local.set("auth",{isAuthenticated:!0,username:e.username}),this._modal.hide()}},{key:"signOut",value:function(){this._store.dispatch((0,o.signOut)()),this._local.set("auth",{isAuthenticated:!1}),this._modal.hide()}},{key:"softSignOut",value:function(){this._store.dispatch((0,o.signOut)(!0)),this._local.set("auth",{isAuthenticated:!1}),this._modal.hide()}}]),e}();a["default"]=new l},{"../reducers/auth":346}],363:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Captcha=a.ReCaptcha=a.ReCaptchaComponent=a.QACaptcha=a.NoCaptcha=a.BaseCaptcha=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../components/form-group"),d=n(c),f=a.BaseCaptcha=function(){function e(){l(this,e)}return s(e,[{key:"init",value:function(e,t,a,n){this._context=e,this._ajax=t,this._include=a,this._snackbar=n}}]),e}(),p=a.NoCaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){return new Promise(function(e){e()})}},{key:"validator",value:function(){return null}},{key:"component",value:function(){return null}}]),t}(f),m=a.QACaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){var e=this;return new Promise(function(t,a){e._ajax.get(e._context.get("CAPTCHA_API")).then(function(a){e.question=a.question,e.helpText=a.help_text,t()},function(){e._snackbar.error(gettext("Failed to load CAPTCHA.")),a()})})}},{key:"validator",value:function(){return[]}},{key:"component",value:function(e){return u["default"].createElement(d["default"],{label:this.question,"for":"id_captcha",labelClass:e.labelClass||"",controlClass:e.controlClass||"",validation:e.form.state.errors.captcha,helpText:this.helpText||null},u["default"].createElement("input",{"aria-describedby":"id_captcha_status",className:"form-control",disabled:e.form.state.isLoading,id:"id_captcha",onChange:e.form.bindInput("captcha"),type:"text",value:e.form.state.captcha}))}}]),t}(f),h=a.ReCaptchaComponent=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;grecaptcha.render("recaptcha",{sitekey:this.props.siteKey,callback:function(t){e.props.binding({target:{value:t}})}})}},{key:"render",value:function(){return u["default"].createElement("div",{id:"recaptcha"})}}]),t}(u["default"].Component),b=a.ReCaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){return this._include.include("https://www.google.com/recaptcha/api.js",!0),new Promise(function(e){var t=function a(){"undefined"==typeof grecaptcha?window.setTimeout(function(){a()},200):e()};t()})}},{key:"validator",value:function(){return[]}},{key:"component",value:function(e){return u["default"].createElement(d["default"],{label:gettext("Please solve the quick test"),"for":"id_captcha",labelClass:e.labelClass||"",controlClass:e.controlClass||"",validation:e.form.state.errors.captcha,helpText:gettext("This test helps us prevent automated spam registrations on our site.")},u["default"].createElement(h,{binding:e.form.bindInput("captcha"),siteKey:this._context.get("SETTINGS").recaptcha_site_key}))}}]),t}(f),v=a.Captcha=function(){function e(){l(this,e)}return s(e,[{key:"init",value:function(e,t,a,n){switch(e.get("SETTINGS").captcha_type){case"no":this._captcha=new p;break;case"qa":this._captcha=new m;break;case"re":this._captcha=new b}this._captcha.init(e,t,a,n)}},{key:"load",value:function(){return this._captcha.load()}},{key:"validator",value:function(){return this._captcha.validator()}},{key:"component",value:function(e){return this._captcha.component(e)}}]),e}();a["default"]=new v},{"../components/form-group":53,react:"react"}],364:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Include=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._staticUrl=e,this._included=[]}},{key:"include",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];this._included.indexOf(e)===-1&&(this._included.push(e),this._include(e,t))}},{key:"_include",value:function(e,t){$.ajax({url:(t?"":this._staticUrl)+e,cache:!0,dataType:"script"})}}]),e}();a["default"]=new o},{}],365:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=window.localStorage,l=a.LocalStorage=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){var t=this;this._prefix=e,this._watchers=[],window.addEventListener("storage",function(e){var a=JSON.parse(e.newValue);t._watchers.forEach(function(t){t.key===e.key&&e.oldValue!==e.newValue&&t.callback(a)})})}},{key:"set",value:function(e,t){o.setItem(this._prefix+e,JSON.stringify(t))}},{key:"get",value:function(e){var t=o.getItem(this._prefix+e);return t?JSON.parse(t):null}},{key:"watch",value:function(e,t){this._watchers.push({key:this._prefix+e,callback:t})}}]),e}();a["default"]=new l},{}],366:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.MobileNavbarDropdown=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("../utils/mount-component"),s=n(l),i=a.MobileNavbarDropdown=function(){function e(){r(this,e)}return o(e,[{key:"init",value:function(e){this._element=e,this._component=null}},{key:"show",value:function(e){this._component===e?this.hide():(this._component=e,(0,s["default"])(e,this._element.id),$(this._element).addClass("open"))}},{key:"showConnected",value:function(e,t){this._component===e?this.hide():(this._component=e,(0,s["default"])(t,this._element.id,!0),$(this._element).addClass("open"))}},{key:"hide",value:function(){$(this._element).removeClass("open"),this._component=null}}]),e}();a["default"]=new i},{"../utils/mount-component":382}],367:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Modal=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("react-dom"),s=n(l),i=e("../utils/mount-component"),u=n(i),c=a.Modal=function(){function e(){r(this,e)}return o(e,[{key:"init",value:function(e){var t=this;this._element=e,this._modal=$(e).modal({show:!1}),this._modal.on("hidden.bs.modal",function(){s["default"].unmountComponentAtNode(t._element)})}},{key:"show",value:function(e){(0,u["default"])(e,this._element.id),this._modal.modal("show")}},{key:"hide",value:function(){this._modal.modal("hide")}}]),e}();a["default"]=new c},{"../utils/mount-component":382,"react-dom":"react-dom"}],368:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e){var t=o(e),a=l(t);if(!a)return null;var n=0;if(t.indexOf("?")>0){var r=t.substr(t.indexOf("?")+1),s=r.split("&").filter(function(e){return"t="===e.substr(0,2)})[0];if(s){var i=s.substr(2).split("m");"s"===i[0].substr(-1)?n+=parseInt(i[0].substr(0,i[0].length-1)):(n+=60*parseInt(i[0]),i[1]&&"s"===i[1].substr(-1)&&(n+=parseInt(i[1].substr(0,i[1].length-1))))}}return{start:n,video:a}}function o(e){var t=e;return"https://"===e.substr(0,8)?t=t.substr(8):"http://"===e.substr(0,7)&&(t=t.substr(7)),"www."===t.substr(0,4)&&(t=t.substr(4)),t}function l(e){if(e.indexOf("youtu")===-1)return null;var t=e.match(i);return t?t[1]:null}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.parseYoutubeUrl=r,a.cleanUrl=o,a.getVideoIdFromUrl=l;var i=new RegExp("^.*(?:(?:youtu.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)??v(?:i)?=|&v(?:i)?=))([^#&?]*).*"),u=a.OneBox=function(){function e(){var t=this;n(this,e),this.render=function(e){e&&(t.highlightCode(e),t.embedYoutubePlayers(e))},this._youtube={}}return s(e,[{key:"highlightCode",value:function(e){for(var t=e.querySelectorAll("pre>code"),a=0;a<t.length;a++){var n=t[a];hljs.highlightBlock(n)}}},{key:"embedYoutubePlayers",value:function(e){for(var t=e.querySelectorAll("p>a"),a=0;a<t.length;a++){var n=t[a],o=n.parentNode,l=1===o.childNodes.length;this._youtube[n.href]||(this._youtube[n.href]=r(n.href));var s=this._youtube[n.href];l&&s&&s.data!==!1&&this.swapYoutubePlayer(n,s)}}},{key:"swapYoutubePlayer",value:function(e,t){var a="https://www.youtube.com/embed/";a+=t.video,a+="?rel=0",t.start&&(a+="&start="+t.start);var n=$('<iframe class="embed-responsive-item" src="'+a+'" allowfullscreen></iframe>');$(e).replaceWith(n),n.wrap('<div class="embed-responsive embed-responsive-16by9"></div>')}}]),e}();a["default"]=new u},{}],369:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.PageTitle=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t){this._indexTitle=e,this._forumName=t}},{key:"set",value:function(e){if(!e)return void(document.title=this._indexTitle||this._forumName);"string"==typeof e&&(e={title:e});var t=e.title;if(e.page>1){var a=interpolate(gettext("page: %(page)s"),{page:e.page},!0);t+=" ("+a+")"}e.parent&&(t+=" | "+e.parent),document.title=t+" | "+this._forumName}}]),e}();a["default"]=new o},{}],370:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Polls=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t){this._ajax=e,this._snackbar=t,this._polls={}}},{key:"start",value:function(e){var t=this;this.stop(e.poll);var a=function n(){t._polls[e.poll]=e,t._ajax.get(e.url,e.data||null).then(function(a){t._polls[e.poll]._stopped||(e.update(a),t._polls[e.poll].timeout=window.setTimeout(n,e.frequency))},function(a){t._polls[e.poll]._stopped||(e.error?e.error(a):t._snackbar.apiError(a))})};e.delayed?this._polls[e.poll]={timeout:window.setTimeout(a,e.frequency)}:a()}},{key:"stop",value:function(e){this._polls[e]&&(window.clearTimeout(this._polls[e].timeout),this._polls[e]._stopped=!0)}}]),e}();a["default"]=new o},{}],371:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Posting=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("react"),s=n(l),i=e("react-dom"),u=n(i),c=e("../components/poll"),d=e("../components/posting"),f=n(d),p=e("../utils/mount-component"),m=n(p),h=a.Posting=function(){function e(){var t=this;r(this,e),this.close=function(){t._isOpen&&!t._isClosing&&(t._isClosing=!0,t._placeholder.removeClass("slide-in"),window.setTimeout(function(){u["default"].unmountComponentAtNode(document.getElementById("posting-mount")),t._isClosing=!1,t._isOpen=!1},300))}}return o(e,[{key:"init",value:function(e,t,a){this._ajax=e,this._snackbar=t,this._placeholder=$(a),this._mode=null,this._isOpen=!1,this._isClosing=!1}},{key:"open",value:function(e){if(this._isOpen===!1)this._mode=e.mode,this._isOpen=e.submit,this._realOpen(e);else if(this._isOpen!==e.submit){var t=gettext("You are already working on other message. Do you want to discard it?");"POLL"==this._mode&&(t=gettext("You are already working on a poll. Do you want to discard it?"));var a=confirm(t);a&&(this._mode=e.mode,this._isOpen=e.submit,this._realOpen(e))}else"REPLY"==this._mode&&"REPLY"==e.mode&&this._realOpen(e)}},{key:"_realOpen",value:function(e){"POLL"==e.mode?(0,m["default"])(s["default"].createElement(c.PollForm,e),"posting-mount"):(0,m["default"])(s["default"].createElement(f["default"],e),"posting-mount"),this._placeholder.addClass("slide-in"),$("html, body").animate({scrollTop:this._placeholder.offset().top},1e3)}}]),e}();a["default"]=new h},{"../components/poll":103,"../components/posting":131,"../utils/mount-component":382,react:"react","react-dom":"react-dom"}],372:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Snackbar=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("../reducers/snackbar"),l=300,s=5e3,i=a.Snackbar=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._store=e,this._timeout=null}},{key:"alert",value:function(e,t){var a=this;this._timeout?(window.clearTimeout(this._timeout),this._store.dispatch((0,o.hideSnackbar)()),this._timeout=window.setTimeout(function(){a._timeout=null,a.alert(e,t)},l)):(this._store.dispatch((0,o.showSnackbar)(e,t)),this._timeout=window.setTimeout(function(){a._store.dispatch((0,o.hideSnackbar)()),a._timeout=null},s))}},{key:"info",value:function(e){this.alert(e,"info")}},{key:"success",value:function(e){this.alert(e,"success")}},{key:"warning",value:function(e){this.alert(e,"warning")}},{key:"error",value:function(e){this.alert(e,"error")}},{key:"apiError",value:function(e){var t=e.detail;t||(t=404===e.status?gettext("Action link is invalid."):gettext("Unknown error has occured.")),403===e.status&&"Permission denied"===t&&(t=gettext("You don't have permission to perform this action.")),this.error(t)}}]),e}();a["default"]=new i},{"../reducers/snackbar":355}],373:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.StoreWrapper=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("redux"),l=a.StoreWrapper=function(){function e(){n(this,e),this._store=null,this._reducers={},this._initialState={}}return r(e,[{key:"addReducer",value:function(e,t,a){this._reducers[e]=t,this._initialState[e]=a}},{key:"init",value:function(){this._store=(0,o.createStore)((0,o.combineReducers)(this._reducers),this._initialState)}},{key:"getStore",value:function(){return this._store}},{key:"getState",value:function(){return this._store.getState()}},{key:"dispatch",value:function(e){return this._store.dispatch(e)}}]),e}();a["default"]=new l},{redux:"redux"}],374:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Zxcvbn=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._include=e,this._isLoaded=!1}},{key:"scorePassword",value:function(e,t){return this._isLoaded?zxcvbn(e,t).score:0}},{key:"load",value:function(){return this._isLoaded?this._loadedPromise():(this._include.include("misago/js/zxcvbn.js"),this._loadingPromise())}},{key:"_loadingPromise",value:function(){var e=this;return new Promise(function(t,a){var n=function r(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;n+=1,n>200?a():"undefined"==typeof zxcvbn?window.setTimeout(function(){r(n)},200):(e._isLoaded=!0,t())};n()})}},{key:"_loadedPromise",value:function(){return new Promise(function(e){e()})}}]),e}();a["default"]=new o},{}],375:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){if(u["default"].render(s["default"].createElement(c.Provider,{store:b["default"].getStore()},s["default"].createElement(_,{message:e.message,expires:e.expires_on?(0,o["default"])(e.expires_on):null})),document.getElementById("page-mount")),"undefined"==typeof t||t){var a=m["default"].get("SETTINGS").forum_name;document.title=gettext("You are banned")+" | "+a,window.history.pushState({},"",m["default"].get("BANNED_URL"))}};var r=e("moment"),o=n(r),l=e("react"),s=n(l),i=e("react-dom"),u=n(i),c=e("react-redux"),d=e("../components/banned-page"),f=n(d),p=e("../index"),m=n(p),h=e("../services/store"),b=n(h),v=function(e){return e.tick},_=(0,c.connect)(v)(f["default"])},{"../components/banned-page":6,"../index":299,"../services/store":373,moment:"moment",react:"react","react-dom":"react-dom","react-redux":"react-redux"}],376:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=[],r=[];if(e.forEach(function(e){r.push(e),r.length===t&&(n.push(r),r=[])}),a!==!1&&r.length>0&&r.length<t)for(var o=r.length;o<t;o++)r.push(a);return r.length&&n.push(r),n}},{}],377:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=[];return e.concat(t).filter(function(e){return a.indexOf(e.id)===-1&&(a.push(e.id),!0)})}},{}],378:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=function(){function e(t,a){n(this,e),this._callback=t,this._count=a}return r(e,[{key:"count",value:function(){this._count-=1,0===this._count&&this._callback()}}]),e}();a["default"]=o},{}],379:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.replace(/[&<>"']/g,function(e){return n[e]})};var n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}},{}],380:[function(e,t,a){"use strict";function n(e){return e.toFixed(1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e>1073741824?n(e/1073741824)+" GB":e>1048576?n(e/1048576)+" MB":e>1024?n(e/1024)+" KB":n(e)+" B"},a.roundSize=n},{}],381:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return n.test($.trim(e))};var n=new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$","i")},{}],382:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],n=document.getElementById(t),r=e.props?e:o["default"].createElement(e,null);n&&(a?s["default"].render(o["default"].createElement(i.Provider,{store:c["default"].getStore()},r),n):s["default"].render(r,n))};var r=e("react"),o=n(r),l=e("react-dom"),s=n(l),i=e("react-redux"),u=e("../services/store"),c=n(u)},{"../services/store":373,react:"react","react-dom":"react-dom","react-redux":"react-redux"}],383:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=function(){function e(t){n(this,e),this.isOrdered=!1,this._items=t||[]}return r(e,[{key:"add",value:function(e,t,a){this._items.push({key:e,item:t,after:a?a.after||null:null,before:a?a.before||null:null})}},{key:"get",value:function(e,t){for(var a=0;a<this._items.length;a++)if(this._items[a].key===e)return this._items[a].item;return t}},{key:"has",value:function(e){return void 0!==this.get(e)}},{key:"values",value:function t(){for(var t=[],e=0;e<this._items.length;e++)t.push(this._items[e].item);return t}},{key:"order",value:function(e){return this.isOrdered||(this._items=this._order(this._items),this.isOrdered=!0),e||"undefined"==typeof e?this.values():this._items}},{key:"orderedValues",value:function(){return this.order(!0)}},{key:"_order",value:function(e){function t(e){var t=-1;r.indexOf(e.key)===-1&&(e.after?(t=r.indexOf(e.after),t!==-1&&(t+=1)):e.before&&(t=r.indexOf(e.before)),t!==-1&&(n.splice(t,0,e),r.splice(t,0,e.key)))}var a=[];e.forEach(function(e){a.push(e.key)});var n=[],r=[];e.forEach(function(e){e.after||e.before||(n.push(e),r.push(e.key))}),e.forEach(function(e){"_end"===e.before&&(n.push(e),r.push(e.key))});for(var o=200;o>0&&a.length!==r.length;)o-=1,e.forEach(t);return n}}]),e}();a["default"]=o},{}],384:[function(e,t,a){"use strict";function n(e,t){return Math.floor(Math.random()*(t-e+1))+e}function r(e,t){for(var a=new Array(n(e,t)),r=0;r<a.length;r++)a[r]=r;return a}Object.defineProperty(a,"__esModule",{value:!0}),a["int"]=n,a.range=r},{}],385:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){window.scrollTo(0,0)}},{}],386:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t={component:e.component||null,childRoutes:[]};e.root?t.childRoutes=[{path:e.root,onEnter:function(t,a){a(null,e.paths[0].path)}}].concat(e.paths):t.childRoutes=e.paths,s["default"].render(o["default"].createElement(i.Provider,{store:d["default"].getStore()},o["default"].createElement(u.Router,{routes:t,history:u.browserHistory})),f)};var r=e("react"),o=n(r),l=e("react-dom"),s=n(l),i=e("react-redux"),u=e("react-router"),c=e("../services/store"),d=n(c),f=document.getElementById("page-mount")},{"../services/store":373,react:"react","react-dom":"react-dom","react-redux":"react-redux","react-router":"react-router"}],387:[function(e,t,a){"use strict";function n(e,t){if(e.indexOf(t)===-1){var a=e.slice();return a.push(t),a}return e}function r(e,t){return e.indexOf(t)>=0?e.filter(function(e){return e!==t}):e}function o(e,t){if(e.indexOf(t)===-1){var a=e.slice();return a.push(t),a}return e.filter(function(e){return e!==t})}Object.defineProperty(a,"__esModule",{value:!0}),a.push=n,a.remove=r,a.toggle=o},{}],388:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){if(e=(e+"").toLowerCase(),t=(t+"").toLowerCase(),t.length<=0)return 0;for(var a=0,n=0,r=t.length;;){if(n=e.indexOf(t,n),!(n>=0))break;a+=1,n+=r}return a}},{}],389:[function(e,t,a){"use strict";function n(){return function(e){if(0===$.trim(e).length)return gettext("This field is required.")}}function r(e){return function(t){if(!d.test(t))return e||gettext("Enter a valid email address.")}}function o(e,t){return function(a){var n="",r=$.trim(a).length;if(r<e)return n=t?t(e,r):ngettext("Ensure this value has at least %(limit_value)s character (it has %(show_value)s).","Ensure this value has at least %(limit_value)s characters (it has %(show_value)s).",e),interpolate(n,{limit_value:e,show_value:r},!0)}}function l(e,t){return function(a){var n="",r=$.trim(a).length;if(r>e)return n=t?t(e,r):ngettext("Ensure this value has at most %(limit_value)s character (it has %(show_value)s).","Ensure this value has at most %(limit_value)s characters (it has %(show_value)s).",e),interpolate(n,{limit_value:e,show_value:r},!0)}}function s(e){var t=function(e){return ngettext("Username must be at least %(limit_value)s character long.","Username must be at least %(limit_value)s characters long.",e)};return o(e,t)}function i(e){var t=function(e){return ngettext("Username cannot be longer than %(limit_value)s character.","Username cannot be longer than %(limit_value)s characters.",e)};return l(e,t)}function u(){return function(e){if(!f.test($.trim(e)))return gettext("Username can only contain latin alphabet letters and digits.")}}function c(e){return function(t){var a=t.length;if(a<e){var n=ngettext("Valid password must be at least %(limit_value)s character long.","Valid password must be at least %(limit_value)s characters long.",e);return interpolate(n,{limit_value:e,show_value:a},!0)}}}Object.defineProperty(a,"__esModule",{value:!0}),a.required=n,a.email=r,a.minLength=o,a.maxLength=l,a.usernameMinLength=s,a.usernameMaxLength=i,a.usernameContent=u,a.passwordMinLength=c;var d=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i,f=new RegExp("^[0-9a-z]+$","i")},{}]},{},[299,300,301,302,303,319,320,321,322,323,324,325,326,342,343,344,345,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341]);
+!function e(t,a,n){function r(l,s){if(!a[l]){if(!t[l]){var i="function"==typeof require&&require;if(!s&&i)return i(l,!0);if(o)return o(l,!0);var u=new Error("Cannot find module '"+l+"'");throw u.code="MODULE_NOT_FOUND",u}var c=a[l]={exports:{}};t[l][0].call(c.exports,function(e){var a=t[l][1][e];return r(a?a:e)},c,c.exports,e,t,a,n)}return a[l].exports}for(var o="function"==typeof require&&require,l=0;l<n.length;l++)r(n[l]);return r}({1:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e(".."),s=n(l),i=e("../utils/escape-html"),u=n(i),c='<a href="%(url)s" target="_blank">%(agreement)s</a>',d=function(e){var t=e.errors,a=e.privacyPolicy,n=e.termsOfService,r=e.onPrivacyPolicyChange,l=e.onTermsOfServiceChange,i=s["default"].get("TERMS_OF_SERVICE_ID"),u=s["default"].get("TERMS_OF_SERVICE_URL"),c=s["default"].get("PRIVACY_POLICY_ID"),d=s["default"].get("PRIVACY_POLICY_URL");return i||c?o["default"].createElement("div",null,o["default"].createElement(f,{agreement:gettext("the terms of service"),checked:null!==n,errors:t.termsOfService,url:u,value:i,onChange:l}),o["default"].createElement(f,{agreement:gettext("the privacy policy"),checked:null!==a,errors:t.privacyPolicy,url:d,value:c,onChange:r})):null},f=function(e){var t=e.agreement,a=e.checked,n=e.errors,r=e.url,l=e.value,s=e.onChange;if(r){var i=interpolate(c,{agreement:(0,u["default"])(t),url:(0,u["default"])(r)},!0),d=interpolate(gettext("I have read and accept %(agreement)s."),{agreement:i},!0);return o["default"].createElement("div",{className:"checkbox legal-footnote"},o["default"].createElement("label",null,o["default"].createElement("input",{checked:a,type:"checkbox",value:l,onChange:s}),o["default"].createElement("span",{dangerouslySetInnerHTML:{__html:d}})),n&&n.map(function(e,t){return o["default"].createElement("div",{className:"help-block errors",key:t},e)}))}};a["default"]=d},{"..":301,"../utils/escape-html":382,react:"react"}],2:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e(".."),s=n(l),i=function(e){var t=e.buttonClassName,a=e.buttonLabel,n=e.formLabel,r=e.header,l=e.labelClassName,i=s["default"].get("SETTINGS").SOCIAL_AUTH;return 0===i.length?null:o["default"].createElement("div",{className:"form-group form-social-auth"},o["default"].createElement(u,{className:l,text:r}),o["default"].createElement("div",{className:"row"},i.map(function(e){var n=e.id,r=e.name,l=e.url,s="btn btn-block btn-default btn-social-"+n,i=interpolate(a,{site:r},!0);return o["default"].createElement("div",{className:t||"col-xs-12",key:n},o["default"].createElement("a",{className:s,href:l},i))})),o["default"].createElement("hr",null),o["default"].createElement(u,{className:l,text:n}))},u=function(e){var t=e.className,a=e.text;return a?o["default"].createElement("h5",{className:t||""},a):null};a["default"]=i},{"..":301,react:"react"}],3:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../services/ajax"),d=n(c),f=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleDecline=function(){if(!a.state.submiting){var e=confirm(gettext("Declining will result in immediate deactivation and deletion of your account. This action is not reversible."));e&&(a.setState({submiting:!0}),d["default"].post(a.props.api,{accept:!1}).then(function(){location.reload(!0)}))}},a.handleAccept=function(){a.state.submiting||(a.setState({submiting:!0}),d["default"].post(a.props.api,{accept:!0}).then(function(){location.reload(!0)}))},a.state={submiting:!1},a}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement("button",{className:"btn btn-default",disabled:this.state.submiting,type:"buton",onClick:this.handleDecline},gettext("Decline")),u["default"].createElement("button",{className:"btn btn-primary",disabled:this.state.submiting,type:"buton",onClick:this.handleAccept},gettext("Accept and continue")))}}]),t}(u["default"].Component);a["default"]=f},{"../services/ajax":364,react:"react"}],4:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Add participant")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("./form"),p=r(f),m=e("./form-group"),h=r(m),b=e("../reducers/participants"),v=n(b),g=e("../reducers/thread"),_=e("../services/ajax"),y=r(_),E=e("../services/modal"),w=r(E),O=e("../services/snackbar"),k=r(O),N=e("../services/store"),x=r(N),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUsernameChange=function(e){a.changeValue("username",e.target.value)},a.state={isLoading:!1,username:""},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.username.trim().length||(k["default"].error(gettext("You have to enter user name.")),!1)}},{key:"send",value:function(){return y["default"].patch(this.props.thread.api.index,[{op:"add",path:"participants",value:this.state.username},{op:"add",path:"acl",value:1}])}},{key:"handleSuccess",value:function(e){x["default"].dispatch((0,g.updateAcl)(e)),x["default"].dispatch(v.replace(e.participants)),k["default"].success(gettext("New participant has been added to thread.")),w["default"].hide()}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog modal-sm",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(h["default"],{"for":"id_username",label:gettext("User to add")},d["default"].createElement("input",{id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.onUsernameChange,type:"text",value:this.state.username}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-block btn-primary",disabled:this.state.isLoading},gettext("Add participant")),d["default"].createElement("button",{className:"btn btn-block btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel"))))))}}]),t}(p["default"]);a["default"]=P},{"../reducers/participants":350,"../reducers/thread":359,"../services/ajax":364,"../services/modal":370,"../services/snackbar":375,"../services/store":376,"./form":55,"./form-group":54,react:"react"}],5:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{user:e.auth.user,signedIn:e.auth.signedIn,signedOut:e.auth.signedOut}}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"refresh",value:function(){window.location.reload()}},{key:"getMessage",value:function(){return this.props.signedIn?interpolate(gettext("You have signed in as %(username)s. Please refresh the page before continuing."),{username:this.props.signedIn.username},!0):this.props.signedOut?interpolate(gettext("%(username)s, you have been signed out. Please refresh the page before continuing."),{username:this.props.user.username},!0):void 0}},{key:"render",value:function(){var e="auth-message";return(this.props.signedIn||this.props.signedOut)&&(e+=" show"),c["default"].createElement("div",{className:e},c["default"].createElement("div",{className:"container"},c["default"].createElement("p",{className:"lead"},this.getMessage()),c["default"].createElement("p",null,c["default"].createElement("button",{className:"btn btn-default",type:"button",onClick:this.refresh},gettext("Reload page")),c["default"].createElement("span",{className:"hidden-xs hidden-sm"}," "+gettext("or press F5 key.")))))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],6:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){return e&&e.id?o(e.avatars,t).url:u["default"].get("BLANK_AVATAR_URL")}function o(e,t){var a=e[0];return e.forEach(function(e){e.size>=t&&(a=e)}),a}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.size||100,a=e.size2x||t;return s["default"].createElement("img",{alt:"",className:e.className||"user-avatar",src:r(e.user,t),srcSet:r(e.user,a),width:t,height:t})},a.getSrc=r,a.resolveAvatarForSize=o;var l=e("react"),s=n(l),i=e(".."),u=n(i)},{"..":301,react:"react"}],7:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getReasonMessage",value:function(){return this.props.message.html?d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.props.message.html}}):d["default"].createElement("p",{className:"lead"},this.props.message.plain)}},{key:"getExpirationMessage",value:function(){if(this.props.expires){if(this.props.expires.isAfter((0,u["default"])())){var e=interpolate(gettext("This ban expires on %(expires_on)s."),{expires_on:this.props.expires.format("LL, LT")},!0),t=interpolate(gettext("This ban expires %(expires_on)s."),{expires_on:this.props.expires.fromNow()},!0);return d["default"].createElement("abbr",{title:e},t)}return gettext("This ban has expired.")}return gettext("This ban is permanent.")}},{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-error page-error-banned"},d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"message-panel"},d["default"].createElement("div",{className:"message-icon"},d["default"].createElement("span",{className:"material-icon"},"highlight_off")),d["default"].createElement("div",{className:"message-body"},this.getReasonMessage(),d["default"].createElement("p",{className:"message-footnote"},this.getExpirationMessage())))))}}]),t}(d["default"].Component);a["default"]=f},{moment:"moment",react:"react"}],8:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){var e="btn "+this.props.className,t=this.props.disabled;return this.props.loading&&(e+=" btn-loading",t=!0),u["default"].createElement("button",{className:e,disabled:t,onClick:this.props.onClick,type:this.props.onClick?"button":"submit"},this.props.children,this.props.loading?u["default"].createElement(d["default"],null):null)}}]),t}(u["default"].Component);a["default"]=f,f.defaultProps={className:"btn-default",type:"submit",loading:!1,disabled:!1,onClick:null}},{"./loader":57,react:"react"}],9:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"categories-list"},o["default"].createElement("ul",{className:"list-group"},o["default"].createElement("li",{className:"list-group-item empty-message"},o["default"].createElement("p",{className:"lead"},gettext("No categories exist or you don't have permission to see them.")))))};var r=e("react"),o=n(r)},{react:"react"}],10:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.categories;return o["default"].createElement("div",{className:"categories-list"},t.map(function(e){return o["default"].createElement(s["default"],{category:e,key:e.id})}))};var r=e("react"),o=n(r),l=e("./category"),s=n(l)},{"./category":11,react:"react"}],11:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a="list-group list-group-category";return t.css_class&&(a+=" list-group-category-has-flavor",a+=" list-group-category-"+t.css_class),o["default"].createElement("ul",{className:a},o["default"].createElement(s["default"],{category:t,isFirst:!0}),t.subcategories.map(function(e){return o["default"].createElement(s["default"],{category:e,isFirst:!1,key:e.id})}))};var r=e("react"),o=n(r),l=e("./list-item"),s=n(l)},{"./list-item":14,react:"react"}],12:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return t.description?o["default"].createElement("div",{className:"category-description",dangerouslySetInnerHTML:{__html:t.description.html}}):null};var r=e("react"),o=n(r)},{react:"react"}],13:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.is_read?"read-status item-read":"read-status item-new"}function o(e){return e.is_closed?e.is_read?gettext("This category has no new posts. (closed)"):gettext("This category has new posts. (closed)"):e.is_read?gettext("This category has no new posts."):gettext("This category has new posts.")}function l(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return i["default"].createElement("div",{className:r(t),title:o(t)},i["default"].createElement("span",{className:"material-icon"},l(t)))},a.getClassName=r,a.getTitle=o,a.getIcon=l;var s=e("react"),i=n(s)},{react:"react"}],14:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isFirst,n="list-group-item";return n+=t.description?" list-group-category-has-description":" list-group-category-no-description",a&&(n+=" list-group-item-first"),t.css_class&&(n+=" list-group-category-has-flavor",n+=" list-group-item-category-"+t.css_class),o["default"].createElement("li",{className:n},o["default"].createElement("div",{className:"row"},o["default"].createElement(s["default"],{category:t}),o["default"].createElement(d["default"],{category:t}),o["default"].createElement(u["default"],{category:t})),o["default"].createElement(p["default"],{category:t,isFirst:a}))};var r=e("react"),o=n(r),l=e("./main"),s=n(l),i=e("./last-thread"),u=n(i),c=e("./stats"),d=n(c),f=e("./subcategories"),p=n(f)},{"./last-thread":15,"./main":16,"./stats":17,"./subcategories":18,react:"react"}],15:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.category;return t.acl.can_browse&&t.acl.can_see_all_threads&&t.last_thread_title?f["default"].createElement("div",{className:"media"},f["default"].createElement("div",{className:"media-left hidden-xs"},f["default"].createElement(o,{category:t})),f["default"].createElement("div",{className:"media-body"},f["default"].createElement("div",{className:"media-heading"},f["default"].createElement("a",{className:"item-title thread-title",href:t.url.last_thread_new,title:t.last_thread_title},t.last_thread_title)),f["default"].createElement("ul",{className:"list-inline"},f["default"].createElement("li",{className:"category-last-thread-poster"},f["default"].createElement(l,{category:t})),f["default"].createElement("li",{className:"divider"},"—"),f["default"].createElement("li",{className:"category-last-thread-date"},f["default"].createElement("a",{href:t.url.last_post},t.last_post_on.fromNow()))))):null}function o(e){var t=e.category;return t.last_poster?f["default"].createElement("a",{className:"last-poster-avatar",href:t.last_poster.url,title:t.last_poster_name},f["default"].createElement(m["default"],{className:"media-object",size:40,user:t.last_poster})):f["default"].createElement("span",{className:"last-poster-avatar",title:t.last_poster_name},f["default"].createElement(m["default"],{className:"media-object",size:40}))}function l(e){var t=e.category;return t.last_poster?f["default"].createElement("a",{className:"item-title",href:t.last_poster.url},t.last_poster_name):f["default"].createElement("span",{className:"item-title"},t.last_poster_name)}function s(e){var t=e.category;return t.acl.can_browse&&t.acl.can_see_all_threads?t.last_thread_title?null:f["default"].createElement(c,{message:gettext("This category is empty. No threads were posted within it so far.")}):null}function i(e){var t=e.category;return t.acl.can_browse?t.acl.can_see_all_threads?null:f["default"].createElement(c,{message:gettext("This category is private. You can see only your own threads within it.")}):null}function u(e){var t=e.category;return t.acl.can_browse?null:f["default"].createElement(c,{message:gettext("This category is protected. You can't browse it's contents.")})}function c(e){var t=e.message;return f["default"].createElement("div",{className:"media category-thread-message"},f["default"].createElement("div",{className:"media-left"},f["default"].createElement("span",{className:"material-icon"},"info_outline")),f["default"].createElement("div",{className:"media-body"},f["default"].createElement("p",null,t)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return f["default"].createElement("div",{className:"col-xs-12 col-sm-6 col-md-4 category-last-thread"},f["default"].createElement(r,{category:t}),f["default"].createElement(s,{category:t}),f["default"].createElement(i,{category:t}),f["default"].createElement(u,{category:t}))},a.LastThread=r,a.LastPosterAvatar=o,a.LastPosterName=l,a.Empty=s,a.Private=i,a.Protected=u,a.Message=c;var d=e("react"),f=n(d),p=e("../../../avatar"),m=n(p)},{"../../../avatar":6,react:"react"}],16:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return o["default"].createElement("div",{className:"col-xs-12 col-sm-6 col-md-6 category-main"},o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement(u["default"],{category:t})),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("h4",{className:"media-heading"},o["default"].createElement("a",{href:t.url.index},t.name)),o["default"].createElement(s["default"],{category:t}))))};var r=e("react"),o=n(r),l=e("./description"),s=n(l),i=e("./icon"),u=n(i)},{"./description":12,"./icon":13,react:"react"}],17:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.threads,a=ngettext("%(threads)s thread","%(threads)s threads",t);return s["default"].createElement("li",{className:"category-stat-threads"},interpolate(a,{threads:t},!0))}function o(e){var t=e.posts,a=ngettext("%(posts)s post","%(posts)s posts",t);return s["default"].createElement("li",{className:"category-stat-posts"},interpolate(a,{posts:t},!0))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category;return s["default"].createElement("div",{className:"col-md-2 hidden-xs hidden-sm"},s["default"].createElement("ul",{className:"list-unstyled category-stats"},s["default"].createElement(r,{threads:t.threads}),s["default"].createElement(o,{posts:t.posts})))},a.Threads=r,a.Posts=o;var l=e("react"),s=n(l),i=e("../../../avatar");n(i)},{"../../../avatar":6,react:"react"}],18:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isFirst;return a?null:0===t.subcategories.length?null:o["default"].createElement("div",{className:"row subcategories-list"},t.subcategories.map(function(e){return o["default"].createElement(s["default"],{category:e,key:e.id})}))};var r=e("react"),o=n(r),l=e("./list-item"),s=n(l)},{"./list-item":19,react:"react"}],19:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a="btn btn-default btn-block btn-sm btn-subcategory";return t.is_read||(a+=" btn-subcategory-new"),l["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3"},l["default"].createElement("a",{className:a,href:t.url.index},l["default"].createElement("span",{className:"material-icon"},r(t)),l["default"].createElement("span",{className:"icon-text"},t.name)))},a.getIcon=r;var o=e("react"),l=n(o)},{react:"react"}],20:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick}}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("moment"),c=n(u),d=e("react"),f=n(d),p=e("./blankslate"),m=n(p),h=e("./categories-list"),b=n(h),v=e("../../index"),g=n(v),_=e("../../services/polls"),y=n(_),E=function O(e){return Object.assign({},e,{last_post_on:e.last_post_on?(0,c["default"])(e.last_post_on):null,subcategories:e.subcategories.map(O)})},w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){a.setState({categories:e.map(E)})},a.state={categories:g["default"].get("CATEGORIES").map(E)},a.startPolling(g["default"].get("CATEGORIES_API")),a}return l(t,e),i(t,[{key:"startPolling",value:function(e){y["default"].start({poll:"categories",url:e,frequency:18e4,update:this.update})}},{key:"render",value:function(){var e=this.state.categories;return 0===e.length?f["default"].createElement(m["default"],null):f["default"].createElement(b["default"],{categories:e})}}]),t}(f["default"].Component);a["default"]=w},{"../../index":301,"../../services/polls":373,"./blankslate":9,"./categories-list":10,moment:"moment",react:"react"}],21:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("select",{className:e.className||"form-control",disabled:e.disabled||!1,id:e.id||null,onChange:e.onChange,value:e.value},e.choices.map(function(e){return o["default"].createElement("option",{disabled:e.disabled||!1,key:e.value,value:e.value},"- - ".repeat(e.level)+e.label)}))};var r=e("react"),o=n(r)},{react:"react"}],22:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=(n(c),e("../button")),f=n(d),p=e("../../services/ajax"),m=n(p),h=e("../../services/snackbar"),b=n(h),v=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.cropAvatar=function(){if(a.state.isLoading)return!1;a.setState({isLoading:!0});var e=a.props.upload?"crop_tmp":"crop_src",t=$(".crop-form"),n=t.cropit("exportZoom"),r=t.cropit("offset");m["default"].post(a.props.user.api.avatar,{avatar:e,crop:{offset:{x:r.x*n,y:r.y*n},zoom:t.cropit("zoom")*n}}).then(function(e){a.props.onComplete(e),b["default"].success(e.detail)},function(e){400===e.status?(b["default"].error(e.detail),a.setState({isLoading:!1})):a.props.showError(e)})},a.state={isLoading:!1,deviceRatio:1},a}return l(t,e),s(t,[{key:"getAvatarSize",value:function(){return this.props.upload?this.props.options.crop_tmp.size:this.props.options.crop_src.size}},{key:"getImagePath",value:function(){return this.props.upload?this.props.dataUrl:this.props.options.crop_src.url}},{key:"componentDidMount",value:function(){for(var e=this,t=$(".crop-form"),a=this.getAvatarSize(),n=t.width();n<a;)a/=2;var r=this.getAvatarSize()/a;t.width(a),t.cropit({width:a,height:a,exportZoom:r,imageState:{src:this.getImagePath()},onImageLoaded:function(){if(e.props.upload){var a=t.cropit("zoom"),n=t.cropit("imageSize");if(n.width>n.height){var r=n.width*a,o=(r-e.getAvatarSize())/-2;t.cropit("offset",{x:o,y:0})}else if(n.width<n.height){var l=n.height*a,s=(l-e.getAvatarSize())/-2;t.cropit("offset",{x:0,y:s})}else t.cropit("offset",{x:0,y:0})}else{var i=e.props.options.crop_src.crop;i&&(t.cropit("zoom",i.zoom),t.cropit("offset",{x:i.x,y:i.y}))}}})}},{key:"componentWillUnmount",value:function(){$(".crop-form").cropit("disable")}},{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement("div",{className:"modal-body modal-avatar-crop"},u["default"].createElement("div",{className:"crop-form"},u["default"].createElement("div",{className:"cropit-preview"}),u["default"].createElement("input",{type:"range",className:"cropit-image-zoom-input"}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},u["default"].createElement(f["default"],{onClick:this.cropAvatar,loading:this.state.isLoading,className:"btn-primary btn-block"},this.props.upload?gettext("Set avatar"):gettext("Crop image")),u["default"].createElement(f["default"],{onClick:this.props.showIndex,disabled:this.state.isLoading,className:"btn-default btn-block"},gettext("Cancel")))))}}]),t}(u["default"].Component);a["default"]=v;
+},{"../../services/ajax":364,"../../services/snackbar":375,"../avatar":6,"../button":8,react:"react"}],23:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Gallery=a.GalleryItem=void 0;var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../avatar"),f=(n(d),e("../button")),p=n(f),m=e("../../index"),h=(n(m),e("../../services/ajax")),b=n(h),v=e("../../services/snackbar"),g=n(v),_=e("../../utils/batch"),y=n(_),E=a.GalleryItem=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.select=function(){n.props.select(n.props.id)},l=a,o(n,l)}return l(t,e),i(t,[{key:"getClassName",value:function(){return this.props.selection===this.props.id?this.props.disabled?"btn btn-avatar btn-disabled avatar-selected":"btn btn-avatar avatar-selected":this.props.disabled?"btn btn-avatar btn-disabled":"btn btn-avatar"}},{key:"render",value:function(){return c["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,onClick:this.select},c["default"].createElement("img",{src:this.props.url}))}}]),t}(c["default"].Component),w=a.Gallery=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"avatars-gallery"},c["default"].createElement("h3",null,this.props.name),c["default"].createElement("div",{className:"avatars-gallery-images"},(0,y["default"])(this.props.images,4,null).map(function(t,a){return c["default"].createElement("div",{className:"row",key:a},t.map(function(t,a){return c["default"].createElement("div",{className:"col-xs-3",key:a},t?c["default"].createElement(E,s({disabled:e.props.disabled,select:e.props.select,selection:e.props.selection},t)):c["default"].createElement("div",{className:"blank-avatar"}))}))})))}}]),t}(c["default"].Component),O=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.select=function(e){a.setState({selection:e})},a.save=function(){return!a.state.isLoading&&(a.setState({isLoading:!0}),void b["default"].post(a.props.user.api.avatar,{avatar:"galleries",image:a.state.selection}).then(function(e){a.setState({isLoading:!1}),g["default"].success(e.detail),a.props.onComplete(e),a.props.showIndex()},function(e){400===e.status?(g["default"].error(e.detail),a.setState({isLoading:!1})):a.props.showError(e)}))},a.state={selection:null,isLoading:!1},a}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",null,c["default"].createElement("div",{className:"modal-body modal-avatar-gallery"},this.props.options.galleries.map(function(t,a){return c["default"].createElement(w,{name:t.name,images:t.images,selection:e.state.selection,disabled:e.state.isLoading,select:e.select,key:a})})),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},c["default"].createElement(p["default"],{onClick:this.save,loading:this.state.isLoading,disabled:!this.state.selection,className:"btn-primary btn-block"},this.state.selection?gettext("Save choice"):gettext("Select avatar")),c["default"].createElement(p["default"],{onClick:this.props.showIndex,disabled:this.state.isLoading,className:"btn-default btn-block"},gettext("Cancel"))))))}}]),t}(c["default"].Component);a["default"]=O},{"../../index":301,"../../services/ajax":364,"../../services/snackbar":375,"../../utils/batch":379,"../avatar":6,"../button":8,react:"react"}],24:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=e("../button"),p=n(f),m=e("../loader"),h=n(m),b=e("../../services/ajax"),v=n(b),g=e("../../services/snackbar"),_=n(g),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.setGravatar=function(){a.callApi("gravatar")},a.setGenerated=function(){a.callApi("generated")},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"callApi",value:function(e){var t=this;return!this.state.isLoading&&(this.setState({isLoading:!0}),void v["default"].post(this.props.user.api.avatar,{avatar:e}).then(function(e){t.setState({isLoading:!1}),_["default"].success(e.detail),t.props.onComplete(e)},function(e){400===e.status?(_["default"].error(e.detail),t.setState({isLoading:!1})):t.props.showError(e)}))}},{key:"getGravatarButton",value:function(){return this.props.options.gravatar?u["default"].createElement(p["default"],{onClick:this.setGravatar,disabled:this.state.isLoading,className:"btn-default btn-block btn-avatar-gravatar"},gettext("Download my Gravatar")):null}},{key:"getCropButton",value:function(){return this.props.options.crop_src?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-crop",disabled:this.state.isLoading,onClick:this.props.showCrop},gettext("Re-crop uploaded image")):null}},{key:"getUploadButton",value:function(){return this.props.options.upload?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-upload",disabled:this.state.isLoading,onClick:this.props.showUpload},gettext("Upload new image")):null}},{key:"getGalleryButton",value:function(){return this.props.options.galleries?u["default"].createElement(p["default"],{className:"btn-default btn-block btn-avatar-gallery",disabled:this.state.isLoading,onClick:this.props.showGallery},gettext("Pick avatar from gallery")):null}},{key:"getAvatarPreview",value:function(){var e={id:this.props.user.id,avatars:this.props.options.avatars};return this.state.isLoading?u["default"].createElement("div",{className:"avatar-preview preview-loading"},u["default"].createElement(d["default"],{size:"200",user:e}),u["default"].createElement(h["default"],null)):u["default"].createElement("div",{className:"avatar-preview"},u["default"].createElement(d["default"],{size:"200",user:e}))}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-index"},u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-md-5"},this.getAvatarPreview()),u["default"].createElement("div",{className:"col-md-7"},this.getGravatarButton(),u["default"].createElement(p["default"],{onClick:this.setGenerated,disabled:this.state.isLoading,className:"btn-default btn-block btn-avatar-generate"},gettext("Generate my individual avatar")),this.getCropButton(),this.getUploadButton(),this.getGalleryButton())))}}]),t}(u["default"].Component);a["default"]=y},{"../../services/ajax":364,"../../services/snackbar":375,"../avatar":6,"../button":8,"../loader":57,react:"react"}],25:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{user:e.auth.user}}Object.defineProperty(a,"__esModule",{value:!0}),a.ChangeAvatarError=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=e("./index"),f=n(d),p=e("./crop"),m=n(p),h=e("./upload"),b=n(h),v=e("./gallery"),g=n(v),_=e("../modal-loader"),y=n(_),E=e("../../reducers/users"),w=e("../../services/ajax"),O=n(w),k=e("../../services/store"),N=n(k),x=a.ChangeAvatarError=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getErrorReason",value:function(){return this.props.reason?c["default"].createElement("p",{dangerouslySetInnerHTML:{__html:this.props.reason}}):null}},{key:"render",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"remove_circle_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},this.props.message),this.getErrorReason(),c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}}]),t}(c["default"].Component),P=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showError=function(e){n.setState({error:e})},n.showIndex=function(){n.setState({component:f["default"]})},n.showUpload=function(){n.setState({component:b["default"]})},n.showCrop=function(){n.setState({component:m["default"]})},n.showGallery=function(){n.setState({component:g["default"]})},n.completeFlow=function(e){N["default"].dispatch((0,E.updateAvatar)(n.props.user,e.avatars)),n.setState({component:f["default"],options:e})},l=a,o(n,l)}return l(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;O["default"].get(this.props.user.api.avatar).then(function(t){e.setState({component:f["default"],options:t,error:null})},function(t){e.showError(t)})}},{key:"getBody",value:function(){return this.state?this.state.error?c["default"].createElement(x,{message:this.state.error.detail,reason:this.state.error.reason}):c["default"].createElement(this.state.component,{options:this.state.options,user:this.props.user,onComplete:this.completeFlow,showError:this.showError,showIndex:this.showIndex,showCrop:this.showCrop,showUpload:this.showUpload,showGallery:this.showGallery}):c["default"].createElement(y["default"],null)}},{key:"getClassName",value:function(){return this.state&&this.state.error?"modal-dialog modal-message modal-change-avatar":"modal-dialog modal-change-avatar"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Change your avatar"))),this.getBody()))}}]),t}(c["default"].Component);a["default"]=P},{"../../reducers/users":363,"../../services/ajax":364,"../../services/store":376,"../modal-loader":60,"./crop":22,"./gallery":23,"./index":24,"./upload":26,react:"react"}],26:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./crop"),d=n(c),f=e("../button"),p=n(f),m=e("../../services/ajax"),h=n(m),b=e("../../services/snackbar"),v=n(b),g=e("../../utils/file-size"),_=n(g),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.pickFile=function(){document.getElementById("avatar-hidden-upload").click()},a.uploadFile=function(){var e=document.getElementById("avatar-hidden-upload").files[0];if(e){var t=a.validateFile(e);if(t)return void v["default"].error(t);a.setState({image:e,preview:URL.createObjectURL(e),progress:0});var n=new FormData;n.append("avatar","upload"),n.append("image",e),h["default"].upload(a.props.user.api.avatar,n,function(e){a.setState({progress:e})}).then(function(e){a.setState({options:e,uploaded:e.detail}),v["default"].info(gettext("Your image has been uploaded and you may now crop it."))},function(e){400===e.status||413===e.status?(v["default"].error(e.detail),a.setState({isLoading:!1,image:null,progress:0})):a.props.showError(e)})}},a.state={image:null,preview:null,progress:0,uploaded:null,dataUrl:null},a}return l(t,e),s(t,[{key:"validateFile",value:function(e){if(e.size>this.props.options.upload.limit)return interpolate(gettext("Selected file is too big. (%(filesize)s)"),{filesize:(0,_["default"])(e.size)},!0);var t=gettext("Selected file type is not supported.");if(this.props.options.upload.allowed_mime_types.indexOf(e.type)===-1)return t;var a=!1,n=e.name.toLowerCase();return this.props.options.upload.allowed_extensions.map(function(e){n.substr(e.length*-1)===e&&(a=!0)}),!a&&t}},{key:"getUploadRequirements",value:function(e){var t=e.allowed_extensions.map(function(e){return e.substr(1)});return interpolate(gettext("%(files)s files smaller than %(limit)s"),{files:t.join(", "),limit:(0,_["default"])(e.limit)},!0)}},{key:"getUploadButton",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-upload"},u["default"].createElement(p["default"],{className:"btn-pick-file",onClick:this.pickFile},u["default"].createElement("div",{className:"material-icon"},"input"),gettext("Select file")),u["default"].createElement("p",{className:"text-muted"},this.getUploadRequirements(this.props.options.upload)))}},{key:"getUploadProgressLabel",value:function(){return interpolate(gettext("%(progress)s % complete"),{progress:this.state.progress},!0)}},{key:"getUploadProgress",value:function(){return u["default"].createElement("div",{className:"modal-body modal-avatar-upload"},u["default"].createElement("div",{className:"upload-progress"},u["default"].createElement("img",{src:this.state.preview}),u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar",role:"progressbar","aria-valuenow":"{this.state.progress}","aria-valuemin":"0","aria-valuemax":"100",style:{width:this.state.progress+"%"}},u["default"].createElement("span",{className:"sr-only"},this.getUploadProgressLabel())))))}},{key:"renderUpload",value:function(){return u["default"].createElement("div",null,u["default"].createElement("input",{type:"file",id:"avatar-hidden-upload",className:"hidden-file-upload",onChange:this.uploadFile}),this.state.image?this.getUploadProgress():this.getUploadButton(),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},u["default"].createElement(p["default"],{onClick:this.props.showIndex,disabled:!!this.state.image,className:"btn-default btn-block"},gettext("Cancel")))))}},{key:"renderCrop",value:function(){return u["default"].createElement(d["default"],{options:this.state.options,user:this.props.user,upload:this.state.uploaded,dataUrl:this.state.preview,onComplete:this.props.onComplete,showError:this.props.showError,showIndex:this.props.showIndex})}},{key:"render",value:function(){return this.state.uploaded?this.renderCrop():this.renderUpload()}}]),t}(u["default"].Component);a["default"]=y},{"../../services/ajax":364,"../../services/snackbar":375,"../../utils/file-size":383,"../button":8,"./crop":22,react:"react"}],27:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.dropdown?"btn btn-default btn-aligned btn-icon btn-dropdown-toggle open hidden-md hidden-lg":"btn btn-default btn-aligned btn-icon btn-dropdown-toggle hidden-md hidden-lg"}},{key:"render",value:function(){return u["default"].createElement("button",{className:this.getClassName(),type:"button",onClick:this.props.toggleNav,"aria-haspopup":"true","aria-expanded":this.props.dropdown?"true":"false"},u["default"].createElement("i",{className:"material-icon"},"menu"))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],28:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display;return t?o["default"].createElement(s["default"],{helpText:gettext("No profile details are editable at this time."),message:gettext("This option is currently unavailable.")}):null};var r=e("react"),o=n(r),l=e("../panel-message"),s=n(l)},{"../panel-message":93,react:"react"}],29:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../select"),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onChange=function(e){var t=n.props,a=t.field,r=t.onChange;r(a.fieldname,e.target.value)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props,t=e.disabled,a=e.field,n=e.value,r=a.input;return"select"===r.type?u["default"].createElement(d["default"],{choices:r.choices,disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,value:n}):"textarea"===r.type?u["default"].createElement("textarea",{className:"form-control",disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,rows:"4",type:"text",value:n}):"text"===r.type?u["default"].createElement("input",{className:"form-control",disabled:t,id:"id_"+a.fieldname,onChange:this.onChange,type:"text",value:n}):null}}]),t}(u["default"].Component);a["default"]=f},{"../select":209,react:"react"}],30:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.disabled,a=e.errors,n=e.fields,r=e.name,l=e.onChange,i=e.value;return o["default"].createElement("fieldset",null,o["default"].createElement("legend",null,r),n.map(function(e){return o["default"].createElement(u["default"],{"for":"id_"+e.fieldname,helpText:e.help_text,key:e.fieldname,label:e.label,validation:a[e.fieldname]},o["default"].createElement(s["default"],{disabled:t,field:e,onChange:l,value:i[e.fieldname]}))}))};var r=e("react"),o=n(r),l=e("./field-input"),s=n(l),i=e("../form-group"),u=n(i)},{"../form-group":54,"./field-input":29,react:"react"}],31:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){var t=e.onCancel,a=e.disabled;return t?d["default"].createElement("button",{className:"btn btn-default",disabled:a,onClick:t,type:"button"},gettext("Cancel")):null}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.CancelButton=i;var c=e("react"),d=n(c),f=e("./fieldset"),p=n(f),m=e("../button"),h=n(m),b=e("../form"),v=n(b),g=e("../../services/ajax"),_=n(g),y=e("../../services/snackbar"),E=n(y),w=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.onChange=function(e,t){a.setState(r({},e,t))},a.state={isLoading:!1,errors:{}};for(var n=e.groups.length,s=0;s<n;s++)for(var i=e.groups[s],u=i.fields.length,c=0;c<u;c++){var d=i.fields[c].fieldname,f=i.fields[c].initial;a.state[d]=f}return a}return s(t,e),u(t,[{key:"send",value:function(){var e=Object.assign({},this.state,{errors:null,isLoading:null});return _["default"].post(this.props.api,e)}},{key:"handleSuccess",value:function(e){this.props.onSuccess(e)}},{key:"handleError",value:function(e){400===e.status?(E["default"].error(gettext("Form contains errors.")),this.setState({errors:e})):E["default"].apiError(e)}},{key:"render",value:function(){var e=this;return d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"panel-body"},this.props.groups.map(function(t,a){return d["default"].createElement(p["default"],{disabled:e.state.isLoading,errors:e.state.errors,fields:t.fields,name:t.name,key:a,onChange:e.onChange,value:e.state})})),d["default"].createElement("div",{className:"panel-footer text-right"},d["default"].createElement(i,{disabled:this.state.isLoading,onCancel:this.props.onCancel})," ",d["default"].createElement(h["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes"))))}}]),t}(v["default"]);a["default"]=w},{"../../services/ajax":364,"../../services/snackbar":375,"../button":8,"../form":55,"./fieldset":30,react:"react"}],32:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.api,a=e.display,n=e.groups,r=e.onCancel,o=e.onSuccess;return a?c["default"].createElement(b["default"],{api:t,groups:n,onCancel:r,onSuccess:o}):null}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.FormDisplay=s;var u=e("react"),c=n(u),d=e("./blankslate"),f=n(d),p=e("./loader"),m=n(p),h=e("./form"),b=n(h),v=e("../../services/ajax"),g=n(v),_=e("../../services/snackbar"),y=n(_),E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={loading:!0,groups:null},a}return l(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;g["default"].get(this.props.api).then(function(t){e.setState({loading:!1,groups:t})},function(t){y["default"].apiError(t),e.props.cancel&&e.props.cancel()})}},{key:"render",value:function(){var e=this.state,t=e.groups,a=e.loading;return c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Edit details"))),c["default"].createElement(m["default"],{display:a}),c["default"].createElement(f["default"],{display:!a&&!t.length}),c["default"].createElement(s,{api:this.props.api,display:!a&&t.length,groups:t,onCancel:this.props.onCancel,onSuccess:this.props.onSuccess}))}}]),t}(c["default"].Component);a["default"]=E},{"../../services/ajax":364,"../../services/snackbar":375,"./blankslate":28,"./form":31,"./loader":33,react:"react"}],33:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display;return t?o["default"].createElement("div",{className:"panel-body"},o["default"].createElement(s["default"],null)):null};var r=e("react"),o=n(r),l=e("../loader"),s=n(l)},{"../loader":57,react:"react"}],34:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.replaceSelection(n.props.execAction)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("button",{className:"btn btn-icon "+this.props.className,disabled:this.props.disabled,onClick:this.onClick,title:this.props.title,type:"button"},this.props.children)}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],35:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a=$.trim(prompt(gettext("Enter name of syntax of your code (optional)")+":"));t("\n\n```"+a+"\n"+e+"\n```\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert code")},e),s["default"].createElement("span",{className:"material-icon"},"functions"))},a.insertCode=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url");n(c)},{"../../../utils/is-url":384,"./action":34,react:"react"}],36:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("*"+e+"*")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Emphase selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_italic"))},a.makeEmphasis=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":34,react:"react"}],37:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){
+t("\n\n- - - - -\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert horizontal ruler")},e),s["default"].createElement("span",{className:"material-icon"},"remove"))},a.insertHr=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":34,react:"react"}],38:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a="",n="";e.length&&((0,d["default"])(e)?a=e:n=e),a=$.trim(prompt(gettext("Enter link to image")+":",a)),n=$.trim(prompt(gettext("Enter image label (optional)")+":",n)),a.length&&t(n.length>0?"!["+n+"]("+a+")":"!("+a+")")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert image")},e),s["default"].createElement("span",{className:"material-icon"},"insert_photo"))},a.insertImage=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url"),d=n(c)},{"../../../utils/is-url":384,"./action":34,react:"react"}],39:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a="",n="";return e.length&&((0,d["default"])(e)?a=e:n=e),a=$.trim(prompt(gettext("Enter link address")+":",a)||""),0!==a.length&&(n=$.trim(prompt(gettext("Enter link label (optional)")+":",n)),void(a.length&&t(n.length>0?"["+n+"]("+a+")":a)))}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert link")},e),s["default"].createElement("span",{className:"material-icon"},"insert_link"))},a.insertLink=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url"),d=n(c)},{"../../../utils/is-url":384,"./action":34,react:"react"}],40:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){var a=$.trim(prompt(gettext("Enter quote autor, prefix usernames with @")+":",a));t(a?'\n\n[quote="'+a+'"]\n'+e+"\n[/quote]\n\n":"\n\n[quote]\n"+e+"\n[/quote]\n\n")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Insert quote")},e),s["default"].createElement("span",{className:"material-icon"},"format_quote"))},a.insertQuote=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i),c=e("../../../utils/is-url");n(c)},{"../../../utils/is-url":384,"./action":34,react:"react"}],41:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("~~"+e+"~~")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Striketrough selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_strikethrough"))},a.makeStriketrough=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":34,react:"react"}],42:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){e.length&&t("**"+e+"**")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],o({execAction:r,title:gettext("Bolder selection")},e),s["default"].createElement("span",{className:"material-icon"},"format_bold"))},a.makeStrong=r;var l=e("react"),s=n(l),i=e("./action"),u=n(i)},{"./action":34,react:"react"}],43:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.item.is_image?_["default"].createElement(i,e):_["default"].createElement(u,e)}function i(e){var t=e.item.url.thumb||e.item.url.index;return _["default"].createElement("div",{className:"editor-attachment-image"},_["default"].createElement("a",{href:e.item.url.index+"?shva=1",style:{backgroundImage:"url('"+t+"?shva=1')"},target:"_blank"}))}function u(e){return _["default"].createElement("div",{className:"editor-attachment-icon"},_["default"].createElement("span",{className:"material-icon"},"insert_drive_file"))}function c(e){return _["default"].createElement("h4",null,_["default"].createElement("a",{className:"item-title",href:e.item.url.index+"?shva=1",target:"_blank"},e.item.filename))}function d(e){var t=null;t=e.item.url.uploader?interpolate(P,{url:(0,w["default"])(e.item.url.uploader),user:(0,w["default"])(e.item.uploader_name)},!0):interpolate(x,{user:(0,w["default"])(e.item.uploader_name)},!0);var a=interpolate(N,{absolute:(0,w["default"])(e.item.uploaded_on.format("LLL")),relative:(0,w["default"])(e.item.uploaded_on.fromNow())},!0),n=interpolate((0,w["default"])(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")),{filetype:e.item.filetype,size:(0,k["default"])(e.item.size),uploader:t,uploaded_on:a},!0);return _["default"].createElement("p",{dangerouslySetInnerHTML:{__html:n}})}function f(e){return _["default"].createElement("div",{className:"editor-attachment-actions"},_["default"].createElement("div",{className:"row"},_["default"].createElement(p,e),_["default"].createElement(m,e),_["default"].createElement(h,e)))}function p(e){return e.item.isRemoved?null:_["default"].createElement("div",{className:"col-xs-6"},_["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onInsert,type:"button"},gettext("Insert")))}function m(e){return e.item.isRemoved&&e.item.acl.can_delete?null:_["default"].createElement("div",{className:"col-xs-6"},_["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onRemove,type:"button"},gettext("Remove")))}function h(e){return e.item.isRemoved?_["default"].createElement("div",{className:"col-xs-12"},_["default"].createElement("button",{className:"btn btn-default btn-sm btn-block",onClick:e.onUndo,type:"button"},gettext("Undo removal"))):null}Object.defineProperty(a,"__esModule",{value:!0});var b=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},v=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Preview=s,a.Image=i,a.Icon=u,a.Filename=c,a.Details=d,a.Actions=f,a.Insert=p,a.Remove=m,a.Undo=h;var g=e("react"),_=n(g),y=e("../../../.."),E=(n(y),e("../../../../utils/escape-html")),w=n(E),O=e("../../../../utils/file-size"),k=n(O),N='<abbr title="%(absolute)s">%(relative)s</abbr>',x='<span class="item-title">%(user)s</span>',P='<a href="%(url)s" class="item-title">%(user)s</a>',j=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onInsert=function(){n.props.replaceSelection(n.insertAttachment)},n.insertAttachment=function(e,t){var a=n.props.item;t(a.is_image?a.url.thumb?"[!["+a.filename+"]("+a.url.thumb+")]("+a.url.index+")":"[!["+a.filename+"]("+a.url.index+")]("+a.url.index+")":"["+a.filename+"]("+a.url.index+")")},n.onRemove=function(){n.updateItem({isRemoved:!0})},n.onUndo=function(){n.updateItem({isRemoved:!1})},n.updateItem=function(e){var t=n.props.attachments.map(function(t){return t.id===n.props.item.id?Object.assign({},t,e):t});n.props.onAttachmentsChange(t)},l=a,o(n,l)}return l(t,e),v(t,[{key:"render",value:function(){return _["default"].createElement("li",{className:"editor-attachment-complete"},_["default"].createElement("div",{className:"row"},_["default"].createElement("div",{className:"col-xs-12 col-sm-8 col-md-9"},_["default"].createElement(s,this.props),_["default"].createElement("div",{className:"editor-attachment-details"},_["default"].createElement(c,this.props),_["default"].createElement(d,this.props))),_["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3 xs-margin-top-half"},_["default"].createElement(f,b({onInsert:this.onInsert,onRemove:this.onRemove,onUndo:this.onUndo},this.props)))))}}]),t}(_["default"].Component);a["default"]=j},{"../../../..":301,"../../../../utils/escape-html":382,"../../../../utils/file-size":383,react:"react"}],44:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../../../utils/escape-html"),d=n(c),f="<strong>%(name)s</strong>",p=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=n.props.attachments.filter(function(e){return e.key!==n.props.item.key});n.props.onAttachmentsChange(e)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=interpolate(f,{name:(0,d["default"])(this.props.item.filename)},!0),t=interpolate(gettext("Error uploading %(filename)s"),{filename:e,progress:this.props.item.progress+"%"},!0);return u["default"].createElement("li",{className:"editor-attachment-error"},u["default"].createElement("div",{className:"editor-attachment-error-icon"},u["default"].createElement("span",{className:"material-icon"},"warning")),u["default"].createElement("div",{className:"editor-attachment-error-message"},u["default"].createElement("h4",{dangerouslySetInnerHTML:{__html:t+":"}}),u["default"].createElement("p",null,this.props.item.error),u["default"].createElement("button",{className:"btn btn-default btn-sm",onClick:this.onClick,type:"button"},gettext("Dismiss"))))}}]),t}(u["default"].Component);a["default"]=p},{"../../../../utils/escape-html":382,react:"react"}],45:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.item.id?o["default"].createElement(s["default"],e):e.item.error?o["default"].createElement(u["default"],e):o["default"].createElement(d["default"],e)};var r=e("react"),o=n(r),l=e("./complete"),s=n(l),i=e("./error"),u=n(i),c=e("./upload"),d=n(c),f=e("../../../.."),p=(n(f),e("../../../../utils/escape-html"));n(p)},{"../../../..":301,"../../../../utils/escape-html":382,"./complete":43,"./error":44,"./upload":46,react:"react"}],46:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=interpolate(i,{name:(0,s["default"])(e.item.filename)},!0),a=interpolate(gettext("Uploading %(filename)s... %(progress)s"),{filename:t,progress:e.item.progress+"%"},!0);return o["default"].createElement("li",{className:"editor-attachment-upload"},o["default"].createElement("div",{className:"editor-attachment-progress-bar"},o["default"].createElement("div",{className:"editor-attachment-progress",style:{width:e.item.progress+"%"}})),o["default"].createElement("p",{className:"editor-attachment-upload-message",dangerouslySetInnerHTML:{__html:a}}))};var r=e("react"),o=n(r),l=e("../../../../utils/escape-html"),s=n(l),i="<strong>%(name)s</strong>"},{"../../../../utils/escape-html":382,react:"react"}],47:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return d["default"].get("user").acl.max_attachment_size?o["default"].createElement("div",{className:"editor-attachments"},o["default"].createElement(s["default"],e),o["default"].createElement(u["default"],e)):null};var r=e("react"),o=n(r),l=e("./list"),s=n(l),i=e("./uploader"),u=n(i),c=e("../../.."),d=n(c)},{"../../..":301,"./list":48,"./uploader":50,react:"react"}],48:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return l["default"].createElement("ul",{className:"list-unstyled editor-attachments-list"},e.attachments.map(function(t){return l["default"].createElement(i["default"],r({item:t,key:t.id||t.key},e))}))};var o=e("react"),l=n(o),s=e("./attachment"),i=n(s)},{"./attachment":45,react:"react"}],49:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../.."),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){document.getElementById("editor-upload-field").click()},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return d["default"].get("user").acl.max_attachment_size?u["default"].createElement("button",{className:"btn btn-icon "+this.props.className,disabled:this.props.disabled,onClick:this.onClick,title:gettext("Upload file"),type:"button"},u["default"].createElement("span",{className:"material-icon"},"file_upload")):null}}]),t}(u["default"].Component);a["default"]=f},{"../../..":301,react:"react"}],50:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(){return"upld-"+Math.round((new Date).getTime())}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getRandomKey=s;var u=e("react"),c=n(u),d=e("moment"),f=n(d),p=e("../../.."),m=n(p),h=e("../../../services/ajax"),b=n(h),v=e("../../../services/snackbar"),g=n(v),_=function(e){function t(){var e,a,n,l;r(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.onChange=function(e){var t=e.target.files[0];if(t){var a={id:null,key:s(),progress:0,error:null,filename:t.name};n.props.onAttachmentsChange([a].concat(n.props.attachments));var r=new FormData;r.append("upload",t),b["default"].upload(m["default"].get("ATTACHMENTS_API"),r,function(e){a.progress=e,n.props.onAttachmentsChange(n.props.attachments.concat())}).then(function(e){e.uploaded_on=(0,f["default"])(e.uploaded_on),Object.assign(a,e),n.props.onAttachmentsChange(n.props.attachments.concat())},function(e){400===e.status||413===e.status?(a.error=e.detail,n.props.onAttachmentsChange(n.props.attachments.concat())):g["default"].apiError(e)})}},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("input",{id:"editor-upload-field",onChange:this.onChange,type:"file"})}}]),t}(c["default"].Component);a["default"]=_},{"../../..":301,"../../../services/ajax":364,"../../../services/snackbar":375,moment:"moment",react:"react"}],51:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){if(!e.canProtect)return null;var t=e.protect?gettext("Protected"):gettext("Protect");return d["default"].createElement("button",{className:"btn btn-icon btn-default btn-protect btn-sm pull-right",disabled:e.disabled,onClick:e.protect?e.onUnprotect:e.onProtect,title:t,type:"button"},d["default"].createElement("span",{className:"material-icon"},e.protect?"lock":"lock_outline"),d["default"].createElement("span",{className:"btn-text hidden-md hidden-lg"},t))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Protect=i;var c=e("react"),d=r(c),f=e("./actions/code"),p=r(f),m=e("./actions/emphasis"),h=r(m),b=e("./actions/hr"),v=r(b),g=e("./actions/image"),_=r(g),y=e("./actions/link"),E=r(y),w=e("./actions/striketrough"),O=r(w),k=e("./actions/strong"),N=r(k),x=e("./actions/quote"),P=r(x),j=e("./attachments"),C=r(j),S=e("./attachments/upload-button"),M=r(S),T=e("./markup-preview"),L=r(T),A=e("./textutils"),R=n(A),I=e("../button"),D=r(I),U=e("../.."),B=r(U),H=e("../../services/ajax"),z=r(H),F=e("../../services/modal"),q=r(F),Y=e("../../services/snackbar"),G=r(Y),V=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onPreviewClick=function(){a.state.isPreviewLoading||(a.setState({isPreviewLoading:!0}),z["default"].post(B["default"].get("PARSE_MARKUP_API"),{post:a.props.value}).then(function(e){q["default"].show(d["default"].createElement(L["default"],{markup:e.parsed})),a.setState({isPreviewLoading:!1})},function(e){400===e.status?G["default"].error(e.detail):G["default"].apiError(e),a.setState({isPreviewLoading:!1})}))},a.replaceSelection=function(e){e(R.getSelectionText(),a._replaceSelection)},a._replaceSelection=function(e){a.props.onChange({target:{value:R.replace(e)}})},a.state={isPreviewLoading:!1},a}return s(t,e),u(t,[{key:"componentDidMount",value:function(){var e=this;$("#editor-textarea").atwho({at:"@",displayTpl:'<li><img src="${avatar}" alt="">${username}</li>',insertTpl:"@${username}",searchKey:"username",callbacks:{remoteFilter:function(e,t){$.getJSON(B["default"].get("MENTION_API"),{q:e},t)}}}),$("#editor-textarea").on("inserted.atwho",function(t,a,n){e.props.onChange(t)})}},{key:"render",value:function(){return d["default"].createElement("div",{className:"editor-border"},d["default"].createElement("textarea",{className:"form-control",value:this.props.value,disabled:this.props.loading,id:"editor-textarea",onChange:this.props.onChange,rows:"9"}),d["default"].createElement("div",{className:"editor-footer"},d["default"].createElement("div",{className:"buttons-list pull-left"},d["default"].createElement(N["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(h["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(O["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(v["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(E["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(_["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(P["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(p["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,replaceSelection:this.replaceSelection}),d["default"].createElement(M["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading})),d["default"].createElement(D["default"],{className:"btn-default btn-sm pull-left",disabled:this.props.loading||this.state.isPreviewLoading,onClick:this.onPreviewClick,type:"button"},gettext("Preview")),d["default"].createElement(D["default"],{className:"btn-primary btn-sm pull-right",loading:this.props.loading},this.props.submitLabel||gettext("Post")),d["default"].createElement("button",{className:"btn btn-default btn-sm pull-right",disabled:this.props.loading,onClick:this.props.onCancel,type:"button"},gettext("Cancel")),d["default"].createElement("div",{className:"clearfix visible-xs-block"}),d["default"].createElement(i,{canProtect:this.props.canProtect,disabled:this.props.loading,onProtect:this.props.onProtect,onUnprotect:this.props.onUnprotect,protect:this.props.protect})),d["default"].createElement(C["default"],{attachments:this.props.attachments,onAttachmentsChange:this.props.onAttachmentsChange,placeholder:this.props.placeholder,replaceSelection:this.replaceSelection}))}}]),t}(d["default"].Component);a["default"]=V},{"../..":301,"../../services/ajax":364,"../../services/modal":370,"../../services/snackbar":375,"../button":8,"./actions/code":35,"./actions/emphasis":36,"./actions/hr":37,"./actions/image":38,"./actions/link":39,"./actions/quote":40,"./actions/striketrough":41,"./actions/strong":42,"./attachments":47,"./attachments/upload-button":49,"./markup-preview":52,"./textutils":53,react:"react"}],52:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"modal-dialog",role:"document"},o["default"].createElement("div",{className:"modal-content"},o["default"].createElement("div",{className:"modal-header"},o["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},o["default"].createElement("span",{"aria-hidden":"true"},"×")),o["default"].createElement("h4",{className:"modal-title"},gettext("Preview message"))),o["default"].createElement("div",{className:"modal-body markup-preview"},o["default"].createElement(s["default"],{markup:e.markup}))))};var r=e("react"),o=n(r),l=e("../misago-markup"),s=n(l)},{"../misago-markup":59,react:"react"}],53:[function(e,t,a){"use strict";function n(){return document.getElementById(d)}function r(){return document.getElementById(d).value}function o(e,t){return{start:e,end:t}}function l(){var e=n();if(document.selection){e.focus();var t=document.selection.createRange(),a=t.text.length;return t.moveStart("character",-e.value.length),o(t.text.length-a,t.text.length)}if(e.selectionStart||"0"==e.selectionStart)return o(e.selectionStart,e.selectionEnd)}function s(){var e=l();return $.trim(r().substring(e.start,e.end))}function i(e){var t=n();if(t.setSelectionRange)t.focus(),t.setSelectionRange(e.start,e.end);else if(t.createTextRange){var a=t.createTextRange();a.collapse(!0),a.moveStart("character",e.start),a.moveEnd("character",e.end),a.select()}}function u(e,t){var a=n(),r=a.value,l=r.substring(0,e.start);return a.value=r.substring(0,e.start)+t+r.substring(e.end),i(o(l.length+t.length,l.length+t.length)),a.value}function c(e){return u(l(),e)}Object.defineProperty(a,"__esModule",{value:!0}),a.getTextarea=n,a.getValue=r,a.getSelectionRange=o,a.getSelection=l,a.getSelectionText=s,a.setSelection=i,a._replace=u,a.replace=c;var d=a.textareaId="editor-textarea"},{}],54:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"isValidated",value:function(){return"undefined"!=typeof this.props.validation}},{key:"getClassName",value:function(){var e="form-group";return this.isValidated()&&(e+=" has-feedback",e+=null===this.props.validation?" has-success":" has-error"),e}},{key:"getFeedback",value:function(){var e=this;return this.props.validation?u["default"].createElement("div",{className:"help-block errors"},this.props.validation.map(function(t,a){return u["default"].createElement("p",{key:e.props["for"]+"FeedbackItem"+a},t)})):null}},{key:"getFeedbackDescription",value:function(){return this.isValidated()?u["default"].createElement("span",{id:this.props["for"]+"_status",className:"sr-only"},this.props.validation?gettext("(error)"):gettext("(success)")):null}},{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("label",{className:"control-label "+(this.props.labelClass||""),htmlFor:this.props["for"]||""},this.props.label+":"),u["default"].createElement("div",{className:this.props.controlClass||""},this.props.children,this.getFeedbackDescription(),this.getFeedback(),this.getHelpText(),this.props.extra||null))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],55:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../utils/validators"),f=e("../services/snackbar"),p=n(f),m=(0,d.required)(),h=function(e){function t(){var e,a,n,s;o(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.bindInput=function(e){return function(t){n.changeValue(e,t.target.value)}},n.changeValue=function(e,t){var a=r({},e,t),o=n.state.errors||{};o[e]=n.validateField(e,a[e]),a.errors=o,n.setState(a)},n.handleSubmit=function(e){if(e&&e.preventDefault(),!n.state.isLoading&&n.clean()){n.setState({isLoading:!0});var t=n.send();t?t.then(function(e){n.setState({isLoading:!1}),n.handleSuccess(e)},function(e){n.setState({isLoading:!1}),n.handleError(e);
+}):n.setState({isLoading:!1})}},s=a,l(n,s)}return s(t,e),i(t,[{key:"validate",value:function(){var e={};if(!this.state.validators)return e;var t={required:this.state.validators.required||this.state.validators,optional:this.state.validators.optional||{}},a=[];for(var n in t.required)t.required.hasOwnProperty(n)&&t.required[n]&&a.push(n);for(var r in t.optional)t.optional.hasOwnProperty(r)&&t.optional[r]&&a.push(r);for(var o in a){var l=a[o],s=this.validateField(l,this.state[l]);null===s?e[l]=null:s&&(e[l]=s)}return e}},{key:"isValid",value:function(){var e=this.validate();for(var t in e)if(e.hasOwnProperty(t)&&null!==e[t])return!1;return!0}},{key:"validateField",value:function(e,t){var a=[];if(!this.state.validators)return a;var n={required:(this.state.validators.required||this.state.validators)[e],optional:(this.state.validators.optional||{})[e]},r=m(t)||!1;if(n.required){if(r)a=[r];else for(var o in n.required){var l=n.required[o](t);l&&a.push(l)}return a.length?a:null}if(r===!1&&n.optional){for(var s in n.optional){var i=n.optional[s](t);i&&a.push(i)}return a.length?a:null}return!1}},{key:"clean",value:function(){return!0}},{key:"send",value:function(){return null}},{key:"handleSuccess",value:function(e){}},{key:"handleError",value:function(e){p["default"].apiError(e)}}]),t}(c["default"].Component);a["default"]=h},{"../services/snackbar":375,"../utils/validators":392,react:"react"}],56:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"isActive",value:function(){return this.props.isControlled?this.props.isActive:!!this.props.path&&0===document.location.pathname.indexOf(this.props.path)}},{key:"getClassName",value:function(){return this.isActive()?(this.props.className||"")+" "+(this.props.activeClassName||"active"):this.props.className||""}},{key:"render",value:function(){return u["default"].createElement("li",{className:this.getClassName()},this.props.children)}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],57:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:e.className||"loader"},o["default"].createElement("div",{className:"loader-spinning-wheel"}))};var r=e("react"),o=n(r)},{react:"react"}],58:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.choices,a=e.onChange,n=e.value;return t?d["default"].createElement(v["default"],{label:gettext("Best answer"),helpText:gettext("Please select the best answer for your newly merged thread. No posts will be deleted during the merge."),"for":"id_best_answer"},d["default"].createElement("select",{className:"form-control",id:"id_best_answer",onChange:a,value:n},t.map(function(e){return d["default"].createElement("option",{value:e[0],key:e[0]},e[1])}))):null}function i(e){var t=e.choices,a=e.onChange,n=e.value;return t?d["default"].createElement(v["default"],{label:gettext("Poll"),helpText:gettext("Please select the poll for your newly merged thread. Rejected polls will be permanently deleted and cannot be recovered."),"for":"id_poll"},d["default"].createElement("select",{className:"form-control",id:"id_poll",onChange:a,value:n},t.map(function(e){return d["default"].createElement("option",{value:e[0],key:e[0]},e[1])}))):null}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.BestAnswerSelect=s,a.PollSelect=i;var c=e("react"),d=n(c),f=e("./button"),p=n(f),m=e("./form"),h=n(m),b=e("./form-group"),v=n(b),g=e("../services/ajax"),_=n(g),y=e("../services/modal"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleSuccess=function(e){a.props.onSuccess(e),E["default"].hide()},a.handleError=function(e){a.props.onError(e)},a.onBestAnswerChange=function(e){a.changeValue("bestAnswer",e.target.value)},a.onPollChange=function(e){a.changeValue("poll",e.target.value)},a.state={isLoading:!1,bestAnswer:"0",poll:"0"},a}return l(t,e),u(t,[{key:"clean",value:function(){if(this.props.polls&&"0"===this.state.poll){var e=confirm(gettext("Are you sure you want to delete all polls?"));return e}return!0}},{key:"send",value:function(){var e=Object.assign({},this.props.data,{best_answer:this.state.bestAnswer,poll:this.state.poll});return _["default"].post(this.props.api,e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Merge threads"))),d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(s,{choices:this.props.bestAnswers,onChange:this.onBestAnswerChange,value:this.state.bestAnswer}),d["default"].createElement(i,{choices:this.props.polls,onChange:this.onPollChange,value:this.state.poll})),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement(p["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Merge threads"))))))}}]),t}(h["default"]);a["default"]=w},{"../services/ajax":364,"../services/modal":370,"./button":8,"./form":55,"./form-group":54,react:"react"}],59:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../services/one-box"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){d["default"].render(this.documentNode)}},{key:"componentDidUpdate",value:function(e,t){d["default"].render(this.documentNode)}},{key:"shouldComponentUpdate",value:function(e,t){return e.markup!==this.props.markup}},{key:"render",value:function(){var e=this;return u["default"].createElement("article",{className:"misago-markup",dangerouslySetInnerHTML:{__html:this.props.markup},ref:function(t){e.documentNode=t}})}}]),t}(u["default"].Component);a["default"]=f},{"../services/one-box":371,react:"react"}],60:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body modal-loader"},u["default"].createElement(d["default"],null))}}]),t}(u["default"].Component);a["default"]=f},{"./loader":57,react:"react"}],61:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./panel-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},this.props.icon||"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.props.message),this.getHelpText(),u["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}}]),t}(d["default"]);a["default"]=f},{"./panel-message":93,react:"react"}],62:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.filter(function(e){return e.results.count>0});return t.map(function(e){return Object.assign({},e,{count:e.results.count,results:e.results.results.slice(0,n)})})};var n=5},{}],63:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.HEADER="HEADER",a.RESULT="RESULT",a.FOOTER="FOOTER"},{}],64:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.children,a=e.onChange,n=e.query;return o["default"].createElement("ul",{className:"dropdown-menu dropdown-search-results",role:"menu"},o["default"].createElement("li",{className:"form-group"},o["default"].createElement(s["default"],{value:n,onChange:a})),t)};var r=e("react"),o=n(r),l=e("./input"),s=n(l)},{"./input":68,react:"react"}],65:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return o["default"].createElement("li",{className:"dropdown-search-message"},gettext("Search returned no results."))};var r=e("react"),o=n(r)},{react:"react"}],66:[function(e,t,a){"use strict";function n(e,t){for(var a=e.length,n=0;n<a;n++){var l=e[n];t.push({provider:l,type:o.HEADER}),r(l,t)}}function r(e,t){for(var a=e.results.length,n=0;n<a;n++){var r=e.results[n];t.push({provider:e,result:r,type:o.RESULT})}t.push({provider:e,type:o.FOOTER})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=[];return n(e,t),t};var o=e("./constants")},{"./constants":63}],67:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){var t=e.isLoading,a=e.onChange,n=e.results,o=e.query;if(!o.trim().length)return l["default"].createElement(u["default"],{onChange:a,query:o});if(n.length){var i=(0,v["default"])(n);return l["default"].createElement(u["default"],{onChange:a,query:o},i.map(function(e){var t=e.type,a=e.provider,n=e.result;return t===s.RESULT?l["default"].createElement(h["default"],r({key:[a.id,t,n.id].join("_")},e)):l["default"].createElement(h["default"],r({key:[a.id,t].join("_"),query:o},e))}))}return t?l["default"].createElement(u["default"],{onChange:a,query:o},l["default"].createElement(p["default"],null)):l["default"].createElement(u["default"],{onChange:a,query:o},l["default"].createElement(d["default"],null))};var o=e("react"),l=n(o),s=e("./constants"),i=e("./dropdown-menu"),u=n(i),c=e("./empty"),d=n(c),f=e("./loader"),p=n(f),m=e("./result"),h=n(m),b=e("./flatten-results"),v=n(b)},{"./constants":63,"./dropdown-menu":64,"./empty":65,"./flatten-results":66,"./loader":69,"./result":72,react:"react"}],68:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.value,a=e.onChange;return o["default"].createElement("input",{"aria-haspopup":"true","aria-expanded":"false",autoComplete:"off",className:"form-control",value:t,onChange:a,placeholder:gettext("Search"),role:"combobox",type:"text"})};var r=e("react"),o=n(r)},{react:"react"}],69:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){e.message;return o["default"].createElement("li",{className:"dropdown-search-loader"},o["default"].createElement(s["default"],null))};var r=e("react"),o=n(r),l=e("../../loader"),s=n(l)},{"../../loader":57,react:"react"}],70:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.query,n=t.url+"?q="+encodeURI(a),r=ngettext('See full "%(provider)s" results page with %(count)s result.','See full "%(provider)s" results page with %(count)s results.',t.count);return o["default"].createElement("li",{className:"dropdown-search-footer"},o["default"].createElement("a",{href:n},interpolate(r,{count:t.count,provider:t.name},!0)))};var r=e("react"),o=n(r)},{react:"react"}],71:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider;return o["default"].createElement("li",{className:"dropdown-search-header"},t.name)};var r=e("react"),o=n(r)},{react:"react"}],72:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.result,n=e.type,r=e.query;return n===l.HEADER?o["default"].createElement(c["default"],{provider:t}):n===l.FOOTER?o["default"].createElement(i["default"],{provider:t,query:r}):o["default"].createElement(f["default"],{provider:t,result:a})};var r=e("react"),o=n(r),l=e("../constants"),s=e("./footer"),i=n(s),u=e("./header"),c=n(u),d=e("./result"),f=n(d)},{"../constants":63,"./footer":70,"./header":71,"./result":73,react:"react"}],73:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.provider,a=e.result;return"threads"===t.id?o["default"].createElement(s["default"],{result:a}):o["default"].createElement(u["default"],{result:a})};var r=e("react"),o=n(r),l=e("./thread"),s=n(l),i=e("./user"),u=n(i)},{"./thread":74,"./user":75,react:"react"}],74:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.result,a=(t.poster,t.thread),n=gettext("Posted by %(poster)s on %(posted_on)s in %(category)s.");return s["default"].createElement("li",null,s["default"].createElement("a",{href:t.url.index,className:"dropdown-search-thread"},s["default"].createElement("h5",null,a.title),s["default"].createElement("small",{className:"dropdown-search-post-content"},$(t.content).text()),s["default"].createElement("small",{className:"dropdown-search-post-footer"},interpolate(n,{category:t.category.name,posted_on:(0,o["default"])(t.posted_on).format("LL"),poster:t.poster_name},!0))))};var r=e("moment"),o=n(r),l=e("react"),s=n(l)},{moment:"moment",react:"react"}],75:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.result,a=t.rank,n=gettext("%(title)s, joined on %(joined_on)s"),r=t.title||a.title||a.name;return s["default"].createElement("li",null,s["default"].createElement("a",{href:t.url,className:"dropdown-search-user"},s["default"].createElement("div",{className:"media"},s["default"].createElement("div",{className:"media-left"},s["default"].createElement(u["default"],{size:38,user:t})),s["default"].createElement("div",{className:"media-body"},s["default"].createElement("h5",{className:"media-heading"},t.username),s["default"].createElement("small",null,interpolate(n,{title:r,joined_on:(0,o["default"])(t.joined_on).format("LL")},!0))))))};var r=e("moment"),o=n(r),l=e("react"),s=n(l),i=e("../../../avatar"),u=n(i)},{"../../../avatar":6,moment:"moment",react:"react"}],76:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../services/ajax"),d=n(c),f=e("../../services/snackbar"),p=n(f),m=e("../.."),h=n(m),b=e("./clean-results"),v=n(b),g=e("./dropdown"),_=n(g),y=function(e){function t(){r(this,t);var e=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));return e.onToggle=function(t){e.setState(function(t,a){return t.isOpen||window.setTimeout(function(){e.container.querySelector("input").focus()},100),{isOpen:!t.isOpen}})},e.onDocumentMouseDown=function(t){for(var a=!0,n=t.target;null!==n&&n!==document;){if(n===e.container)return void(a=!1);n=n.parentNode}a&&e.setState({isOpen:!1})},e.onEscape=function(t){"Escape"===t.key&&e.setState({isOpen:!1})},e.onChange=function(t){var a=t.target.value;e.setState({query:a}),e.loadResults(a.trim())},e.state={isLoading:!1,isOpen:!1,query:"",results:[]},e.intervalId=null,e}return l(t,e),s(t,[{key:"componentDidMount",value:function(){document.addEventListener("mousedown",this.onDocumentMouseDown),document.addEventListener("keydown",this.onEscape)}},{key:"componentWillUnmount",value:function(){document.removeEventListener("mousedown",this.onDocumentMouseDown),document.removeEventListener("keydown",this.onEscape)}},{key:"loadResults",value:function(e){var t=this;if(e.length){var a=300+300*Math.random();this.intervalId&&window.clearTimeout(this.intervalId),this.setState({isLoading:!0}),this.intervalId=window.setTimeout(function(){d["default"].get(h["default"].get("SEARCH_API"),{q:e}).then(function(e){t.setState({intervalId:null,isLoading:!1,results:(0,v["default"])(e)})},function(e){p["default"].apiError(e),t.setState({intervalId:null,isLoading:!1,results:[]})})},a)}}},{key:"render",value:function(){var e=this,t="navbar-search dropdown";return this.state.isOpen&&(t+=" open"),u["default"].createElement("div",{className:t,ref:function(t){return e.container=t}},u["default"].createElement("a",{"aria-haspopup":"true","aria-expanded":"false",className:"navbar-icon","data-toggle":"dropdown",href:h["default"].get("SEARCH_URL"),onClick:this.onToggle},u["default"].createElement("i",{className:"material-icon"},"search")),u["default"].createElement(_["default"],{isLoading:this.state.isLoading,onChange:this.onChange,results:this.state.results,query:this.state.query}))}}]),t}(u["default"].Component);a["default"]=y},{"../..":301,"../../services/ajax":364,"../../services/snackbar":375,"./clean-results":62,"./dropdown":67,react:"react"}],77:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),o["default"].createElement(s["default"],null))};var r=e("react"),o=n(r),l=e("../../panel-loader"),s=n(l)},{"../../panel-loader":92,react:"react"}],78:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../panel-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.options.next_on?interpolate(gettext("You will be able to change your username %(next_change)s."),{next_change:this.props.options.next_on.fromNow()},!0):gettext("You have used up available name changes.")}},{key:"render",value:function(){return u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),u["default"].createElement(d["default"],{helpText:this.getHelpText(),message:gettext("You can't change your username at the moment.")}))}}]),t}(u["default"].Component);a["default"]=f},{"../../panel-message":93,react:"react"}],79:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../../services/ajax"),g=r(v),_=e("../../../services/snackbar"),y=r(_),E=e("../../../utils/validators"),w=n(E),O=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={username:"",validators:{username:[w.usernameContent(),w.usernameMinLength(e.options.length_min),w.usernameMaxLength(e.options.length_max)]},isLoading:!1},a}return s(t,e),i(t,[{key:"getHelpText",value:function(){var e=[];if(this.props.options.changes_left>0){var t=ngettext("You can change your username %(changes_left)s more time.","You can change your username %(changes_left)s more times.",this.props.options.changes_left);e.push(interpolate(t,{changes_left:this.props.options.changes_left},!0))}if(this.props.user.acl.name_changes_expire>0){var a=ngettext("Used changes become available again after %(name_changes_expire)s day.","Used changes become available again after %(name_changes_expire)s days.",this.props.user.acl.name_changes_expire);e.push(interpolate(a,{name_changes_expire:this.props.user.acl.name_changes_expire},!0))}return e.length?e.join(" "):null}},{key:"clean",value:function(){var e=this.validate();return e.username?(y["default"].error(e.username[0]),!1):this.state.username.trim()!==this.props.user.username||(y["default"].info(gettext("Your new username is same as current one.")),!1)}},{key:"send",value:function(){return g["default"].post(this.props.user.api.username,{username:this.state.username})}},{key:"handleSuccess",value:function(e){this.setState({username:""}),this.props.complete(e.username,e.slug,e.options)}},{key:"handleError",value:function(e){y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Change username"))),c["default"].createElement("div",{className:"panel-body"},c["default"].createElement(b["default"],{label:gettext("New username"),"for":"id_username",helpText:this.getHelpText()},c["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change username")))))}}]),t}(m["default"]);a["default"]=O},{"../../../services/ajax":364,"../../../services/snackbar":375,"../../../utils/validators":392,"../../button":8,"../../form":55,"../../form-group":54,react:"react"}],80:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=e("./form-loading"),p=n(f),m=e("./form-locked"),h=n(m),b=e("./form"),v=n(b),g=e("../../username-history/root"),_=n(g),y=e("../../../index"),E=n(y),w=e("../../../reducers/username-history"),O=e("../../../reducers/users"),k=e("../../../services/ajax"),N=n(k),x=e("../../../services/page-title"),P=n(x),j=e("../../../services/snackbar"),C=n(j),S=e("../../../services/store"),M=n(S),T=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onComplete=function(e,t,n){a.setState({options:n}),M["default"].dispatch((0,w.addNameChange)({username:e,slug:t},a.props.user,a.props.user)),M["default"].dispatch((0,O.updateUsername)(a.props.user,e,t)),C["default"].success(gettext("Your username has been changed successfully."))},a.state={isLoaded:!1,options:null},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;P["default"].set({title:gettext("Change username"),parent:gettext("Change your options")}),Promise.all([N["default"].get(this.props.user.api.username),N["default"].get(E["default"].get("USERNAME_CHANGES_API"),{user:this.props.user.id})]).then(function(t){M["default"].dispatch((0,w.hydrate)(t[1].results)),e.setState({isLoaded:!0,options:{changes_left:t[0].changes_left,
+length_min:t[0].length_min,length_max:t[0].length_max,next_on:t[0].next_on?(0,u["default"])(t[0].next_on):null}})})}},{key:"getChangeForm",value:function(){return this.state.isLoaded?0===this.state.options.changes_left?d["default"].createElement(h["default"],{options:this.state.options}):d["default"].createElement(v["default"],{complete:this.onComplete,options:this.state.options,user:this.props.user}):d["default"].createElement(p["default"],null)}},{key:"render",value:function(){return d["default"].createElement("div",null,this.getChangeForm(),d["default"].createElement(_["default"],{changes:this.props["username-history"],isLoaded:this.state.isLoaded}))}}]),t}(d["default"].Component);a["default"]=T},{"../../../index":301,"../../../reducers/username-history":362,"../../../reducers/users":363,"../../../services/ajax":364,"../../../services/page-title":372,"../../../services/snackbar":375,"../../../services/store":376,"../../username-history/root":279,"./form":79,"./form-loading":77,"./form-locked":78,moment:"moment",react:"react"}],81:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../../services/ajax"),p=n(f),m=e("../../services/page-title"),h=n(m),b=e("../../services/snackbar"),v=n(b),g=e("../../services/store"),_=(n(g),e("../..")),y=n(_),E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onPasswordChange=function(e){a.setState({password:e.target.value})},a.handleSubmit=function(e){e.preventDefault();var t=a.state,n=t.isLoading,r=t.password,o=a.props.user;return 0==r.length?(v["default"].error(gettext("Enter your password to confirm account deletion.")),!1):!n&&(a.setState({isLoading:!0}),void p["default"].post(o.api["delete"],{password:r}).then(function(e){window.location.href=y["default"].get("MISAGO_PATH")},function(e){a.setState({isLoading:!1}),e.password?v["default"].error(e.password[0]):v["default"].apiError(e)}))},a.state={isLoading:!1,password:""},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){h["default"].set({title:gettext("Delete account"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"panel panel-danger panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Delete account"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement("p",{className:"lead"},gettext("You are going to delete your account. This action is nonreversible, and will result in following data being deleted:")),u["default"].createElement("p",null,"- ",gettext("Stored IP addresses associated with content that you have posted will be deleted.")),u["default"].createElement("p",null,"- ",gettext("Your username will become available for other user to rename to or for new user to register their account with.")),u["default"].createElement("p",null,"- ",gettext("Your e-mail will become available for use in new account registration.")),u["default"].createElement("hr",null),u["default"].createElement("p",null,gettext("All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts."))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement("div",{className:"input-group"},u["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,name:"password-confirmation",type:"password",placeholder:gettext("Enter your password to confirm account deletion."),value:this.state.password,onChange:this.onPasswordChange}),u["default"].createElement("span",{className:"input-group-btn"},u["default"].createElement(d["default"],{className:"btn-danger",loading:this.state.isLoading},gettext("Delete my account")))))))}}]),t}(u["default"].Component);a["default"]=E},{"../..":301,"../../services/ajax":364,"../../services/page-title":372,"../../services/snackbar":375,"../../services/store":376,"../button":8,react:"react"}],82:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("moment"),d=n(c),f=e("../button"),p=n(f),m=e("../../services/ajax"),h=n(m),b=e("../../services/page-title"),v=n(b),g=e("../../services/snackbar"),_=n(g),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleLoadDownloads=function(){h["default"].get(a.props.user.api.data_downloads).then(function(e){a.setState({isLoading:!1,downloads:e})},function(e){_["default"].apiError(e)})},a.handleRequestDataDownload=function(){a.setState({isSubmiting:!0}),h["default"].post(a.props.user.api.request_data_download).then(function(){a.handleLoadDownloads(),_["default"].success(gettext("Your request for data download has been registered.")),a.setState({isSubmiting:!1})},function(e){console.log(e),_["default"].apiError(e),a.setState({isSubmiting:!1})})},a.state={isLoading:!1,isSubmiting:!1,downloads:[]},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){v["default"].set({title:gettext("Download your data"),parent:gettext("Change your options")}),this.handleLoadDownloads()}},{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Download your data"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement("p",null,gettext('To download your data from the site, click the "Request data download" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.')),u["default"].createElement("p",null,gettext("The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired."))),u["default"].createElement("table",{className:"table"},u["default"].createElement("thead",null,u["default"].createElement("tr",null,u["default"].createElement("th",null,gettext("Requested on")),u["default"].createElement("th",{className:"col-md-4"},gettext("Download")))),u["default"].createElement("tbody",null,this.state.downloads.map(function(e){return u["default"].createElement("tr",{key:e.id},u["default"].createElement("td",{style:E},(0,d["default"])(e.requested_on).fromNow()),u["default"].createElement("td",null,u["default"].createElement(k,{exportFile:e.file,status:e.status})))}),0==this.state.downloads.length?u["default"].createElement("tr",null,u["default"].createElement("td",{colSpan:"2"},gettext("You have no data downloads."))):null)),u["default"].createElement("div",{className:"panel-footer text-right"},u["default"].createElement(p["default"],{className:"btn-primary",loading:this.state.isSubmiting,type:"button",onClick:this.handleRequestDataDownload},gettext("Request data download")))))}}]),t}(u["default"].Component);a["default"]=y;var E={verticalAlign:"middle"},w=0,O=1,k=function(e){var t=e.exportFile,a=e.status;return a===w||a===O?u["default"].createElement(p["default"],{className:"btn-info btn-sm btn-block",disabled:!0,type:"button"},gettext("Download is being prepared")):t?u["default"].createElement("a",{className:"btn btn-success btn-sm btn-block",href:t},gettext("Download your data")):u["default"].createElement(p["default"],{className:"btn-default btn-sm btn-block",disabled:!0,type:"button"},gettext("Download is expired"))}},{"../../services/ajax":364,"../../services/page-title":372,"../../services/snackbar":375,"../button":8,moment:"moment",react:"react"}],83:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../edit-details"),d=n(c),f=e("../../services/page-title"),p=n(f),m=e("../../services/snackbar"),h=n(m),b=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onSuccess=function(){h["default"].info(gettext("Your details have been updated."))},l=a,o(n,l)}return l(t,e),s(t,[{key:"componentDidMount",value:function(){p["default"].set({title:gettext("Edit details"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement(d["default"],{api:this.props.user.api.edit_details,onSuccess:this.onSuccess})}}]),t}(u["default"].Component);a["default"]=b},{"../../services/page-title":372,"../../services/snackbar":375,"../edit-details":32,react:"react"}],84:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../form"),p=n(f),m=e("../form-group"),h=n(m),b=e("../select"),v=n(b),g=e("../yes-no-switch"),_=n(g),y=e("../../reducers/auth"),E=e("../../services/ajax"),w=n(E),O=e("../../services/page-title"),k=n(O),N=e("../../services/snackbar"),x=n(N),P=e("../../services/store"),j=n(P),C=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,is_hiding_presence:e.user.is_hiding_presence,limits_private_thread_invites_to:e.user.limits_private_thread_invites_to,subscribe_to_started_threads:e.user.subscribe_to_started_threads,subscribe_to_replied_threads:e.user.subscribe_to_replied_threads,errors:{}},a.privateThreadInvitesChoices=[{value:0,icon:"help_outline",label:gettext("Everybody")},{value:1,icon:"done_all",label:gettext("Users I follow")},{value:2,icon:"highlight_off",label:gettext("Nobody")}],a.subscribeToChoices=[{value:0,icon:"star_border",label:gettext("No")},{value:1,icon:"star_half",label:gettext("Notify")},{value:2,icon:"star",label:gettext("Notify with e-mail")}],a}return l(t,e),s(t,[{key:"send",value:function(){return w["default"].post(this.props.user.api.options,{is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,subscribe_to_started_threads:this.state.subscribe_to_started_threads,subscribe_to_replied_threads:this.state.subscribe_to_replied_threads})}},{key:"handleSuccess",value:function(){j["default"].dispatch((0,y.patch)({is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,subscribe_to_started_threads:this.state.subscribe_to_started_threads,subscribe_to_replied_threads:this.state.subscribe_to_replied_threads})),x["default"].success(gettext("Your forum options have been changed."))}},{key:"handleError",value:function(e){400===e.status?x["default"].error(gettext("Please reload page and try again.")):x["default"].apiError(e)}},{key:"componentDidMount",value:function(){k["default"].set({title:gettext("Forum options"),parent:gettext("Change your options")})}},{key:"render",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change forum options"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement("fieldset",null,u["default"].createElement("legend",null,gettext("Privacy settings")),u["default"].createElement(h["default"],{label:gettext("Hide my presence"),helpText:gettext("If you hide your presence, only members with permission to see hidden users will see when you are online."),"for":"id_is_hiding_presence"},u["default"].createElement(_["default"],{id:"id_is_hiding_presence",disabled:this.state.isLoading,iconOn:"visibility_off",iconOff:"visibility",labelOn:gettext("Hide my presence from other users"),labelOff:gettext("Show my presence to other users"),onChange:this.bindInput("is_hiding_presence"),value:this.state.is_hiding_presence})),u["default"].createElement(h["default"],{label:gettext("Private thread invitations"),"for":"id_limits_private_thread_invites_to"},u["default"].createElement(v["default"],{id:"id_limits_private_thread_invites_to",disabled:this.state.isLoading,onChange:this.bindInput("limits_private_thread_invites_to"),value:this.state.limits_private_thread_invites_to,choices:this.privateThreadInvitesChoices}))),u["default"].createElement("fieldset",null,u["default"].createElement("legend",null,gettext("Automatic subscriptions")),u["default"].createElement(h["default"],{label:gettext("Threads I start"),"for":"id_subscribe_to_started_threads"},u["default"].createElement(v["default"],{id:"id_subscribe_to_started_threads",disabled:this.state.isLoading,onChange:this.bindInput("subscribe_to_started_threads"),value:this.state.subscribe_to_started_threads,choices:this.subscribeToChoices})),u["default"].createElement(h["default"],{label:gettext("Threads I reply to"),"for":"id_subscribe_to_replied_threads"},u["default"].createElement(v["default"],{id:"id_subscribe_to_replied_threads",disabled:this.state.isLoading,onChange:this.bindInput("subscribe_to_replied_threads"),value:this.state.subscribe_to_replied_threads,choices:this.subscribeToChoices})))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes")))))}}]),t}(p["default"]);a["default"]=C},{"../../reducers/auth":349,"../../services/ajax":364,"../../services/page-title":372,"../../services/snackbar":375,"../../services/store":376,"../button":8,"../form":55,"../form-group":54,"../select":209,"../yes-no-switch":299,react:"react"}],85:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("div",{className:"list-group nav-side"},e.options.map(function(t){return s["default"].createElement(i.Link,{to:e.baseUrl+t.component+"/",className:"list-group-item",activeClassName:"active",key:t.component},s["default"].createElement("span",{className:"material-icon"},t.icon),t.name)}))}function o(e){return s["default"].createElement("ul",{className:e.className||"dropdown-menu stick-to-bottom",role:"menu"},e.options.map(function(t){return s["default"].createElement(c["default"],{path:e.baseUrl+t.component+"/",key:t.component},s["default"].createElement(i.Link,{to:e.baseUrl+t.component+"/",onClick:e.hideNav},s["default"].createElement("span",{className:"material-icon hidden-sm"},t.icon),t.name))}))}Object.defineProperty(a,"__esModule",{value:!0}),a.SideNav=r,a.CompactNav=o;var l=e("react"),s=n(l),i=e("react-router"),u=e("../li"),c=n(u),d=e("../../index");n(d)},{"../../index":301,"../li":56,react:"react","react-router":"react-router"}],86:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick,user:e.auth.user,"username-history":e["username-history"]}}function i(){var e=[{path:S["default"].get("USERCP_URL")+"forum-options/",component:(0,f.connect)(s)(w["default"])},{path:S["default"].get("USERCP_URL")+"edit-details/",component:(0,f.connect)(s)(g["default"])},{path:S["default"].get("USERCP_URL")+"change-username/",component:(0,f.connect)(s)(k["default"])},{path:S["default"].get("USERCP_URL")+"sign-in-credentials/",component:(0,f.connect)(s)(x["default"])}];return S["default"].get("ENABLE_DOWNLOAD_OWN_DATA")&&e.push({path:S["default"].get("USERCP_URL")+"download-data/",component:(0,f.connect)(s)(y["default"])}),S["default"].get("ENABLE_DELETE_OWN_ACCOUNT")&&e.push({path:S["default"].get("USERCP_URL")+"delete-account/",component:(0,f.connect)(s)(b["default"])}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("../dropdown-toggle"),m=(n(p),e("./navs")),h=e("./delete-account"),b=n(h),v=e("./edit-details"),g=n(v),_=e("./download-data"),y=n(_),E=e("./forum-options"),w=n(E),O=e("./change-username/root"),k=n(O),N=e("./sign-in-credentials/root"),x=n(N),P=e("../with-dropdown"),j=n(P),C=e("../../index"),S=n(C),M=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-options"},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:"page-header"},d["default"].createElement("div",{className:"container"},d["default"].createElement("h1",null,gettext("Change your options"))),d["default"].createElement("div",{className:"page-tabs visible-xs-block visible-sm-block"},d["default"].createElement("div",{className:"container"},d["default"].createElement(m.CompactNav,{className:"nav nav-pills",baseUrl:S["default"].get("USERCP_URL"),options:S["default"].get("USER_OPTIONS")}))))),d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm"},d["default"].createElement(m.SideNav,{baseUrl:S["default"].get("USERCP_URL"),options:S["default"].get("USER_OPTIONS")})),d["default"].createElement("div",{className:"col-md-9"},this.props.children))))}}]),t}(j["default"]);a["default"]=M},{"../../index":301,"../dropdown-toggle":27,"../with-dropdown":298,"./change-username/root":80,"./delete-account":81,"./download-data":82,"./edit-details":83,"./forum-options":84,"./navs":85,"./sign-in-credentials/root":90,react:"react","react-redux":"react-redux"}],87:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e("../../../index"),s=n(l),i=function(){return o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Change email or password"))),o["default"].createElement("div",{className:"panel-body panel-message-body"},o["default"].createElement("div",{className:"message-icon"},o["default"].createElement("span",{className:"material-icon"},"info_outline")),o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",{className:"lead"},gettext("You need to set a password for your account to be able to change your username or email.")),o["default"].createElement("p",{className:"help-block"},o["default"].createElement("a",{className:"btn btn-primary",href:s["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Set password"))))))};a["default"]=i},{"../../../index":301,react:"react"}],88:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../../services/ajax"),g=r(v),_=e("../../../services/snackbar"),y=r(_),E=e("../../../utils/validators"),w=n(E),O=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={new_email:"",password:"",validators:{new_email:[w.email()],password:[]},isLoading:!1},a}return s(t,e),i(t,[{key:"clean",value:function(){var e=this.validate(),t=[this.state.new_email.trim().length,this.state.password.trim().length];return t.indexOf(0)!==-1?(y["default"].error(gettext("Fill out all fields.")),!1):!e.new_email||(y["default"].error(e.new_email[0]),!1)}},{key:"send",value:function(){return g["default"].post(this.props.user.api.change_email,{new_email:this.state.new_email,password:this.state.password})}},{key:"handleSuccess",value:function(e){this.setState({new_email:"",password:""}),y["default"].success(e.detail)}},{key:"handleError",value:function(e){400===e.status?e.new_email?y["default"].error(e.new_email):y["default"].error(e.password):y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("input",{type:"type",style:{display:"none"}}),c["default"].createElement("input",{type:"password",style:{display:"none"}}),c["default"].createElement("div",{className:"panel panel-default panel-form"},c["default"].createElement("div",{className:"panel-heading"},c["default"].createElement("h3",{className:"panel-title"},gettext("Change e-mail address"))),c["default"].createElement("div",{className:"panel-body"},c["default"].createElement(b["default"],{label:gettext("New e-mail"),"for":"id_new_email"},c["default"].createElement("input",{type:"text",id:"id_new_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_email"),value:this.state.new_email})),c["default"].createElement("hr",null),c["default"].createElement(b["default"],{label:gettext("Your current password"),"for":"id_confirm_email"},c["default"].createElement("input",{type:"password",id:"id_confirm_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change e-mail")))))}}]),t}(m["default"]);a["default"]=O},{"../../../services/ajax":364,"../../../services/snackbar":375,"../../../utils/validators":392,"../../button":8,"../../form":55,"../../form-group":54,react:"react"}],89:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../../services/ajax"),v=n(b),g=e("../../../services/snackbar"),_=n(g),y=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={new_password:"",repeat_password:"",password:"",validators:{new_password:[],repeat_password:[],password:[]},isLoading:!1},a}return l(t,e),s(t,[{key:"clean",value:function(){var e=this.validate(),t=[this.state.new_password.trim().length,this.state.repeat_password.trim().length,this.state.password.trim().length];return t.indexOf(0)!==-1?(_["default"].error(gettext("Fill out all fields.")),!1):e.new_password?(_["default"].error(e.new_password[0]),!1):this.state.new_password===this.state.repeat_password||(_["default"].error(gettext("New passwords are different.")),!1)}},{key:"send",value:function(){return v["default"].post(this.props.user.api.change_password,{new_password:this.state.new_password,password:this.state.password})}},{key:"handleSuccess",value:function(e){this.setState({new_password:"",repeat_password:"",password:""}),_["default"].success(e.detail)}},{key:"handleError",value:function(e){400===e.status?e.new_password?_["default"].error(e.new_password):_["default"].error(e.password):_["default"].apiError(e)}},{key:"render",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("input",{type:"type",style:{display:"none"}}),u["default"].createElement("input",{type:"password",style:{display:"none"}}),u["default"].createElement("div",{className:"panel panel-default panel-form"},u["default"].createElement("div",{className:"panel-heading"},u["default"].createElement("h3",{className:"panel-title"},gettext("Change password"))),u["default"].createElement("div",{className:"panel-body"},u["default"].createElement(h["default"],{label:gettext("New password"),"for":"id_new_password"},u["default"].createElement("input",{type:"password",id:"id_new_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_password"),value:this.state.new_password})),u["default"].createElement(h["default"],{label:gettext("Repeat password"),"for":"id_repeat_password"},u["default"].createElement("input",{type:"password",id:"id_repeat_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("repeat_password"),value:this.state.repeat_password})),u["default"].createElement("hr",null),u["default"].createElement(h["default"],{label:gettext("Your current password"),"for":"id_confirm_password"},u["default"].createElement("input",{type:"password",id:"id_confirm_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),u["default"].createElement("div",{className:"panel-footer"},u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change password")))))}}]),t}(p["default"]);a["default"]=y},{"../../../services/ajax":364,"../../../services/snackbar":375,"../../button":8,"../../form":55,"../../form-group":54,react:"react"}],90:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),
+Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change-email"),d=n(c),f=e("./change-password"),p=n(f),m=e("../../../index"),h=n(m),b=e("../../../services/page-title"),v=n(b),g=e("./UnusablePasswordMessage"),_=n(g),y=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){v["default"].set({title:gettext("Change email or password"),parent:gettext("Change your options")})}},{key:"render",value:function(){return this.props.user.has_usable_password?u["default"].createElement("div",null,u["default"].createElement(d["default"],{user:this.props.user}),u["default"].createElement(p["default"],{user:this.props.user}),u["default"].createElement("p",{className:"message-line"},u["default"].createElement("span",{className:"material-icon"},"warning"),u["default"].createElement("a",{href:h["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Change forgotten password")))):u["default"].createElement(_["default"],null)}}]),t}(u["default"].Component);a["default"]=y},{"../../../index":301,"../../../services/page-title":372,"./UnusablePasswordMessage":87,"./change-email":88,"./change-password":89,react:"react"}],91:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../utils/string-count"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.copy&&this.props.copy.length&&1===(0,d["default"])(this.props.copy,"<p")&&this.props.copy.indexOf("<br")===-1?"page-lead lead":"page-lead"}},{key:"render",value:function(){return this.props.copy&&this.props.copy.length?u["default"].createElement("div",{className:this.getClassName(),dangerouslySetInnerHTML:{__html:this.props.copy}}):null}}]),t}(u["default"].Component);a["default"]=f},{"../utils/string-count":391,react:"react"}],92:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"panel-body panel-body-loading"},u["default"].createElement(d["default"],{className:"loader loader-spaced"}))}}]),t}(u["default"].Component);a["default"]=f},{"./loader":57,react:"react"}],93:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getHelpText",value:function(){return this.props.helpText?u["default"].createElement("p",{className:"help-block"},this.props.helpText):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"panel-body panel-message-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},this.props.icon||"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.props.message),this.getHelpText()))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],94:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../add-participant"),d=n(c),f=e("../../services/modal"),p=n(f),m=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){p["default"].show(u["default"].createElement(d["default"],{thread:n.props.thread}))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.thread.acl.can_add_participants?u["default"].createElement("div",{className:"col-xs-12 col-sm-3"},u["default"].createElement("button",{className:"btn btn-default btn-block",onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},"person_add"),gettext("Add participant"))):null}}]),t}(u["default"].Component);a["default"]=m},{"../../services/modal":370,"../add-participant":4,react:"react"}],95:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function o(e,t){m["default"].patch(e.api.index,[{op:"remove",path:"participants",value:t.id}]).then(function(){b["default"].success(gettext("You have left this thread.")),window.setTimeout(function(){window.location=f["default"].get("PRIVATE_THREADS_URL")},3e3)},function(e){b["default"].apiError(e)})}function l(e,t){m["default"].patch(e.api.index,[{op:"remove",path:"participants",value:t.id},{op:"add",path:"acl",value:1}]).then(function(e){g["default"].dispatch((0,c.updateAcl)(e)),g["default"].dispatch(u.replace(e.participants));var a=gettext("%(user)s has been removed from this thread.");b["default"].success(interpolate(a,{user:t.username},!0))},function(e){b["default"].apiError(e)})}function s(e,t){m["default"].patch(e.api.index,[{op:"replace",path:"owner",value:t.id},{op:"add",path:"acl",value:1}]).then(function(e){g["default"].dispatch((0,c.updateAcl)(e)),g["default"].dispatch(u.replace(e.participants));var a=gettext("%(user)s has been made new thread owner.");b["default"].success(interpolate(a,{user:t.username},!0))},function(e){b["default"].apiError(e)})}Object.defineProperty(a,"__esModule",{value:!0}),a.leave=o,a.remove=l,a.changeOwner=s;var i=e("../../../reducers/participants"),u=r(i),c=e("../../../reducers/thread"),d=e("../../.."),f=n(d),p=e("../../../services/ajax"),m=n(p),h=e("../../../services/snackbar"),b=n(h),v=e("../../../services/store"),g=n(v)},{"../../..":301,"../../../reducers/participants":350,"../../../reducers/thread":359,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376}],96:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.isOwner;return t?l["default"].createElement("li",{className:"dropdown-header dropdown-header-owner"},l["default"].createElement("span",{className:"material-icon"},"start"),l["default"].createElement("span",{className:"icon-text"},gettext("Thread owner"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.participant,a="btn btn-default";return t.is_owner&&(a="btn btn-primary"),a+=" btn-user btn-block",l["default"].createElement("div",{className:"col-xs-12 col-sm-3 col-md-2 participant-card"},l["default"].createElement("div",{className:"dropdown"},l["default"].createElement("button",{"aria-haspopup":"true","aria-expanded":"false",className:a,"data-toggle":"dropdown",type:"button"},l["default"].createElement(f["default"],{size:"34",user:t}),l["default"].createElement("span",{className:"btn-text"},t.username)),l["default"].createElement("ul",{className:"dropdown-menu stick-to-bottom"},l["default"].createElement(r,{isOwner:t.is_owner}),l["default"].createElement("li",{className:"dropdown-header"}),l["default"].createElement("li",null,l["default"].createElement("a",{href:t.url},gettext("See profile"))),l["default"].createElement("li",{role:"separator",className:"divider"}),l["default"].createElement(i["default"],e),l["default"].createElement(c["default"],e))))},a.UserStatus=r;var o=e("react"),l=n(o),s=e("./make-owner"),i=n(s),u=e("./remove"),c=n(u),d=e("../../avatar"),f=n(d)},{"../../avatar":6,"./make-owner":98,"./remove":99,react:"react"}],97:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.participants,a=e.thread,n=e.user,r=e.userIsOwner;return o["default"].createElement("div",{className:"participants-cards"},o["default"].createElement("div",{className:"row"},t.map(function(e){return o["default"].createElement(s["default"],{key:e.id,participant:e,thread:a,user:n,userIsOwner:r})})))};var r=e("react"),o=n(r),l=e("./card"),s=n(l)},{"./card":96,react:"react"}],98:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./actions"),d=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onClick=function(){var e=!1;if(a.isUser)e=confirm(gettext("Are you sure you want to take over this thread?"));else{var t=gettext("Are you sure you want to change thread owner to %(user)s?");e=confirm(interpolate(t,{user:a.props.participant.username},!0))}e&&(0,c.changeOwner)(a.props.thread,a.props.participant)},a.isUser=e.participant.id===e.user.id,a}return l(t,e),s(t,[{key:"render",value:function(){return this.props.participant.is_owner?null:this.props.thread.acl.can_change_owner?u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},gettext("Make owner"))):null}}]),t}(u["default"].Component);a["default"]=d},{"./actions":95,react:"react"}],99:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./actions"),d=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onClick=function(){var e=!1;if(a.isUser)e=confirm(gettext("Are you sure you want to leave this thread?"));else{var t=gettext("Are you sure you want to remove %(user)s from this thread?");e=confirm(interpolate(t,{user:a.props.participant.username},!0))}e&&(a.isUser?(0,c.leave)(a.props.thread,a.props.participant):(0,c.remove)(a.props.thread,a.props.participant))},a.isUser=e.participant.id===e.user.id,a}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.user.acl.can_moderate_private_threads;return this.props.userIsOwner||this.isUser||e?u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},this.isUser?gettext("Leave thread"):gettext("Remove"))):null}}]),t}(u["default"].Component);a["default"]=d},{"./actions":95,react:"react"}],100:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){return t[0].id===e.id}Object.defineProperty(a,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return e.participants.length?i["default"].createElement("div",{className:"panel panel-default panel-participants"},i["default"].createElement("div",{className:"panel-body"},i["default"].createElement(f["default"],l({userIsOwner:o(e.user,e.participants)},e)),i["default"].createElement("div",{className:"row"},i["default"].createElement(c["default"],{thread:e.thread}),i["default"].createElement("div",{className:"col-xs-12 col-sm-9"},i["default"].createElement("p",null,m.getParticipantsCopy(e.participants)))))):null},a.getUserIsOwner=o;var s=e("react"),i=r(s),u=e("./add-participant"),c=r(u),d=e("./cards-list"),f=r(d),p=e("./utils"),m=n(p)},{"./add-participant":94,"./cards-list":97,"./utils":101,react:"react"}],101:[function(e,t,a){"use strict";function n(e){var t=e.length,a=ngettext("This thread has %(users)s participant.","This thread has %(users)s participants.",t);return interpolate(a,{users:t},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a.getParticipantsCopy=n},{}],102:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LABELS=a.STYLES=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../services/zxcvbn"),d=n(c),f=a.STYLES=["progress-bar-danger","progress-bar-warning","progress-bar-warning","progress-bar-primary","progress-bar-success"],p=a.LABELS=[gettext("Entered password is very weak."),gettext("Entered password is weak."),gettext("Entered password is average."),gettext("Entered password is strong."),gettext("Entered password is very strong.")],m=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a._score=0,a._password=null,a._inputs=[],a.state={loaded:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;d["default"].load().then(function(){e.setState({loaded:!0})})}},{key:"getScore",value:function(e,t){var a=this,n=!1;return e!==this._password&&(n=!0),t.length!==this._inputs.length?n=!0:t.map(function(e,t){e.trim()!==a._inputs[t]&&(n=!0)}),n&&(this._score=d["default"].scorePassword(e,t),this._password=e,this._inputs=t.map(function(e){return e.trim()})),this._score}},{key:"render",value:function(){if(!this.state.loaded)return null;var e=this.getScore(this.props.password,this.props.inputs);return u["default"].createElement("div",{className:"help-block password-strength"},u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar "+f[e],style:{width:20+20*e+"%"},role:"progress-bar","aria-valuenow":e,"aria-valuemin":"0","aria-valuemax":"4"},u["default"].createElement("span",{className:"sr-only"},p[e]))),u["default"].createElement("p",{className:"text-small"},p[e]))}}]),t}(u["default"].Component);a["default"]=m},{"../services/zxcvbn":377,react:"react"}],103:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(){for(var e="";12!=e.length;)e=Math.random().toString(36).replace(/[^a-zA-Z0-9]+/g,"").substr(1,12);return e}Object.defineProperty(a,"__esModule",{value:!0}),a.PollChoice=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.generateRandomHash=s;var u=e("react"),c=n(u),d=function(e){function t(){var e,a,n,l;r(this,t);for(var i=arguments.length,u=Array(i),c=0;c<i;c++)u[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(u))),n.onAdd=function(){var e=n.props.choices.slice();e.push({hash:s(),label:""}),n.props.setChoices(e)},n.onChange=function(e,t){var a=n.props.choices.map(function(a){return a.hash===e&&(a.label=t),a});n.props.setChoices(a)},n.onDelete=function(e){var t=n.props.choices.filter(function(t){return t.hash!==e});n.props.setChoices(t)},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"poll-choices-control"},c["default"].createElement("ul",{className:"list-group"},this.props.choices.map(function(t){return c["default"].createElement(f,{canDelete:e.props.choices.length>2,choice:t,disabled:e.props.disabled,key:t.hash,onChange:e.onChange,onDelete:e.onDelete})})),c["default"].createElement("button",{className:"btn btn-default btn-sm",disabled:this.props.disabled,onClick:this.onAdd,type:"button"},gettext("Add choice")))}}]),t}(c["default"].Component);a["default"]=d;var f=a.PollChoice=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onChange=function(e){n.props.onChange(n.props.choice.hash,e.target.value)},n.onDelete=function(){var e=confirm(gettext("Are you sure you want to delete this choice?"));e&&n.props.onDelete(n.props.choice.hash)},l=a,o(n,l)}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("li",{className:"list-group-item"},c["default"].createElement("button",{className:"btn",disabled:!this.props.canDelete||this.props.disabled,onClick:this.onDelete,title:gettext("Delete this choice"),type:"button"},c["default"].createElement("span",{className:"material-icon"},"close")),c["default"].createElement("input",{disabled:this.props.disabled,maxLength:"255",placeholder:gettext("choice label"),type:"text",onChange:this.onChange,value:this.props.choice.label}))}}]),t}(c["default"].Component)},{react:"react"}],104:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.isEdit?null:d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(_["default"],{label:gettext("Make voting public"),helpText:gettext("Making voting public will allow everyone to access detailed list of votes, showing which users voted for which choices and at which times. This option can't be changed after poll's creation. Moderators may see voting details for all polls."),"for":"id_is_public"},d["default"].createElement(E["default"],{id:"id_is_public",disabled:e.disabled,iconOn:"visibility",iconOff:"visibility_off",labelOn:gettext("Votes are public"),labelOff:gettext("Votes are hidden"),onChange:e.bindInput("is_public"),value:e.value})))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.PollPublicSwitch=i;var c=e("react"),d=r(c),f=e("./choices-control"),p=r(f),m=e("../../button"),h=r(m),b=e("../../form"),v=r(b),g=e("../../form-group"),_=r(g),y=e("../../yes-no-switch"),E=r(y),w=e("../../../reducers/poll"),O=n(w),k=e("../../../services/ajax"),N=r(k),x=e("../../../services/posting"),P=r(x),j=e("../../../services/snackbar"),C=r(j),S=e("../../../services/store"),M=r(S),T=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.setChoices=function(e){var t=Object.assign({},t,{choices:null});a.setState({choices:e,errors:t})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard poll?"));e&&P["default"].close()};var n=e.poll||{question:"",choices:[{hash:"choice-10000",label:""},{hash:"choice-20000",label:""}],length:0,allowed_choices:1,allow_revotes:0,is_public:0};return a.state={isLoading:!1,isEdit:!!n.question,question:n.question,choices:n.choices,length:n.length,allowed_choices:n.allowed_choices,allow_revotes:n.allow_revotes,is_public:n.is_public,validators:{question:[],choices:[],length:[],allowed_choices:[]},errors:{}},a}return s(t,e),u(t,[{key:"send",value:function(){var e={question:this.state.question,choices:this.state.choices,length:this.state.length,allowed_choices:this.state.allowed_choices,allow_revotes:this.state.allow_revotes,is_public:this.state.is_public};return this.state.isEdit?N["default"].put(this.props.poll.api.index,e):N["default"].post(this.props.thread.api.poll,e)}},{key:"handleSuccess",value:function(e){M["default"].dispatch(O.replace(e)),this.state.isEdit?C["default"].success(gettext("Poll has been edited.")):C["default"].success(gettext("Poll has been posted.")),P["default"].close()}},{key:"handleError",value:function(e){400===e.status?(e.non_field_errors&&(e.allowed_choices=e.non_field_errors),this.setState({errors:Object.assign({},e)}),C["default"].error(gettext("Form contains errors."))):C["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"poll-form"},d["default"].createElement("div",{className:"container"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"panel panel-default panel-form"},d["default"].createElement("div",{className:"panel-body"},d["default"].createElement("fieldset",null,d["default"].createElement("legend",null,gettext("Question and choices")),d["default"].createElement(_["default"],{label:gettext("Poll question"),"for":"id_questions",validation:this.state.errors.question},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_questions",onChange:this.bindInput("question"),type:"text",maxLength:"255",value:this.state.question})),d["default"].createElement(_["default"],{label:gettext("Available choices"),validation:this.state.errors.choices},d["default"].createElement(p["default"],{choices:this.state.choices,disabled:this.state.isLoading,setChoices:this.setChoices}))),d["default"].createElement("fieldset",null,d["default"].createElement("legend",null,gettext("Voting")),d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(_["default"],{label:gettext("Poll length"),helpText:gettext("Enter number of days for which voting in this poll should be possible or zero to run this poll indefinitely."),"for":"id_length",validation:this.state.errors.length},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_length",onChange:this.bindInput("length"),type:"text",value:this.state.length}))),d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(_["default"],{label:gettext("Allowed choices"),"for":"id_allowed_choices",validation:this.state.errors.allowed_choices},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_allowed_choices",onChange:this.bindInput("allowed_choices"),type:"text",maxLength:"255",value:this.state.allowed_choices})))),d["default"].createElement("div",{className:"row"},d["default"].createElement(i,{bindInput:this.bindInput,disabled:this.state.isLoading,isEdit:this.state.isEdit,value:this.state.is_public}),d["default"].createElement("div",{className:"col-xs-12 col-sm-6"},d["default"].createElement(_["default"],{label:gettext("Allow vote changes"),"for":"id_allow_revotes"},d["default"].createElement(E["default"],{id:"id_allow_revotes",disabled:this.state.isLoading,iconOn:"check",iconOff:"close",labelOn:gettext("Allow participants to change their vote"),labelOff:gettext("Don't allow participants to change their vote"),onChange:this.bindInput("allow_revotes"),value:this.state.allow_revotes})))))),d["default"].createElement("div",{className:"panel-footer text-right"},d["default"].createElement("button",{className:"btn btn-default",disabled:this.state.isLoading,onClick:this.onCancel,type:"button"},gettext("Cancel"))," ",d["default"].createElement(h["default"],{className:"btn-primary",loading:this.state.isLoading},this.state.isEdit?gettext("Save changes"):gettext("Post poll")))))))}}]),t}(v["default"]);a["default"]=T},{"../../../reducers/poll":351,"../../../services/ajax":364,"../../../services/posting":374,"../../../services/snackbar":375,"../../../services/store":376,"../../button":8,"../../form":55,"../../form-group":54,"../../yes-no-switch":299,"./choices-control":103,react:"react"}],105:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PollForm=a.Poll=void 0;var r=e("./poll"),o=n(r),l=e("./form"),s=n(l);a.Poll=o["default"],a.PollForm=s["default"]},{"./form":104,"./poll":107}],106:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=interpolate((0,m["default"])(gettext("Posted by %(poster)s %(posted_on)s.")),{poster:o(e.poll),posted_on:l(e.poll)},!0);return f["default"].createElement("li",{className:"poll-info-creation",dangerouslySetInnerHTML:{__html:t}})}function o(e){return e.url.poster?interpolate(v,{url:(0,m["default"])(e.url.poster),user:(0,m["default"])(e.poster_name)},!0):interpolate(b,{user:(0,m["default"])(e.poster_name)},!0)}function l(e){return interpolate(h,{absolute:(0,m["default"])(e.posted_on.format("LLL")),relative:(0,m["default"])(e.posted_on.fromNow())},!0)}function s(e){if(!e.poll.length)return null;var t=interpolate((0,m["default"])(gettext("Voting ends %(ends_on)s.")),{ends_on:i(e.poll)},!0);return f["default"].createElement("li",{className:"poll-info-ends-on",dangerouslySetInnerHTML:{__html:t}})}function i(e){return interpolate(h,{absolute:(0,m["default"])(e.endsOn.format("LLL")),relative:(0,m["default"])(e.endsOn.fromNow())},!0)}function u(e){var t=ngettext("%(votes)s vote.","%(votes)s votes.",e.votes),a=interpolate(t,{votes:e.votes},!0);return f["default"].createElement("li",{className:"poll-info-votes"},a)}function c(e){return e.poll.is_public?f["default"].createElement("li",{
+className:"poll-info-public"},gettext("Votes are public.")):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return f["default"].createElement("ul",{className:"list-unstyled list-inline poll-details"},f["default"].createElement(u,{votes:e.poll.votes}),f["default"].createElement(s,{poll:e.poll}),f["default"].createElement(c,{poll:e.poll}),f["default"].createElement(r,{poll:e.poll}))},a.PollCreation=r,a.getPoster=o,a.getPostedOn=l,a.PollLength=s,a.getEndsOn=i,a.PollVotes=u,a.PollIsPublic=c;var d=e("react"),f=n(d),p=e("../../utils/escape-html"),m=n(p),h='<abbr title="%(absolute)s">%(relative)s</abbr>',b='<span class="item-title">%(user)s</span>',v='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../utils/escape-html":382,react:"react"}],107:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return!!e.length&&(0,p["default"])().isAfter(e.endsOn)}Object.defineProperty(a,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getIsPollOver=s;var c=e("react"),d=n(c),f=e("moment"),p=n(f),m=e("./results"),h=n(m),b=e("./voting"),v=n(b),g=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.showResults=function(){a.setState({showResults:!0})},a.showVoting=function(){a.setState({showResults:!1})};var n=!0;return e.user.id&&!e.poll.hasSelectedChoices&&(n=!1),a.state={showResults:n},a}return l(t,e),u(t,[{key:"render",value:function(){if(!this.props.thread.poll)return null;var e=s(this.props.poll);return e||!this.props.poll.acl.can_vote||this.state.showResults?d["default"].createElement(h["default"],i({isPollOver:e,showVoting:this.showVoting},this.props)):d["default"].createElement(v["default"],i({showResults:this.showResults},this.props))}}]),t}(d["default"].Component);a["default"]=g},{"./results":109,"./voting":113,moment:"moment",react:"react"}],108:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=0;return e.choice.votes&&e.poll.votes&&(t=Math.ceil(100*e.choice.votes/e.poll.votes)),u["default"].createElement("dl",{className:"dl-horizontal"},u["default"].createElement("dt",null,e.choice.label),u["default"].createElement("dd",null,u["default"].createElement("div",{className:"progress"},u["default"].createElement("div",{className:"progress-bar",role:"progressbar","aria-valuenow":t,"aria-valuemin":"0","aria-valuemax":"100",style:{width:t+"%"}},u["default"].createElement("span",{className:"sr-only"},l(e.votes,e.proc)))),u["default"].createElement("ul",{className:"list-unstyled list-inline poll-chart"},u["default"].createElement(o,{proc:t,votes:e.choice.votes}),u["default"].createElement(s,{selected:e.choice.selected}))))}function o(e){return u["default"].createElement("li",{className:"poll-chart-votes"},l(e.votes,e.proc))}function l(e,t){var a=ngettext("%(votes)s vote, %(proc)s% of total.","%(votes)s votes, %(proc)s% of total.",e);return interpolate(a,{votes:e,proc:t},!0)}function s(e){return e.selected?u["default"].createElement("li",{className:"poll-chart-selected"},u["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Your choice.")):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return u["default"].createElement("div",{className:"poll-choices-bars"},e.poll.choices.map(function(t){return u["default"].createElement(r,{choice:t,key:t.hash,poll:e.poll})}))},a.PollChoice=r,a.ChoiceVotes=o,a.getVotesLabel=l,a.UserChoice=s;var i=e("react"),u=n(i)},{react:"react"}],109:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"panel panel-default panel-poll"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("h2",null,e.poll.question),o["default"].createElement(d["default"],{poll:e.poll}),o["default"].createElement(s["default"],{poll:e.poll}),o["default"].createElement(u["default"],{isPollOver:e.isPollOver,poll:e.poll,showVoting:e.showVoting,thread:e.thread})))};var r=e("react"),o=n(r),l=e("./chart"),s=n(l),i=e("./options"),u=n(i),c=e("../info"),d=n(c)},{"../info":106,"./chart":108,"./options":111,react:"react"}],110:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.isLoading?v["default"].createElement(O["default"],null):e.error?v["default"].createElement(E["default"],{icon:"error_outline",message:e.error}):v["default"].createElement(i,{data:e.data})}function i(e){return v["default"].createElement("div",{className:"modal-body modal-poll-votes"},v["default"].createElement("ul",{className:"list-unstyled votes-details"},e.data.map(function(e){return v["default"].createElement(u,m({key:e.hash},e))})))}function u(e){return v["default"].createElement("li",null,v["default"].createElement("h4",null,e.label),v["default"].createElement(c,{votes:e.votes}),v["default"].createElement(d,{voters:e.voters}),v["default"].createElement("hr",null))}function c(e){var t=ngettext("%(votes)s user has voted for this choice.","%(votes)s users have voted for this choice.",e.votes),a=interpolate(t,{votes:e.votes},!0);return v["default"].createElement("p",null,a)}function d(e){return e.voters.length?v["default"].createElement("ul",{className:"list-unstyled"},e.voters.map(function(e){return v["default"].createElement(f,m({key:e.username},e))})):null}function f(e){return e.url?v["default"].createElement("li",null,v["default"].createElement("a",{className:"item-title",href:e.url},e.username)," ",v["default"].createElement(p,{voted_on:e.voted_on})):v["default"].createElement("li",null,v["default"].createElement("strong",null,e.username)," ",v["default"].createElement(p,{voted_on:e.voted_on}))}function p(e){return v["default"].createElement("abbr",{className:"text-muted",title:e.voted_on.format("LLL")},e.voted_on.fromNow())}Object.defineProperty(a,"__esModule",{value:!0});var m=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},h=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalBody=s,a.ChoicesList=i,a.ChoiceDetails=u,a.VotesCount=c,a.VotesList=d,a.Voter=f,a.VoteDate=p;var b=e("react"),v=n(b),g=e("moment"),_=n(g),y=e("../../modal-message"),E=n(y),w=e("../../modal-loader"),O=n(w),k=e("../../../services/ajax"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!0,error:null,data:[]},a}return l(t,e),h(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.poll.api.votes).then(function(t){var a=t.map(function(e){return Object.assign({},e,{voters:e.voters.map(function(e){return Object.assign({},e,{voted_on:(0,_["default"])(e.voted_on)})})})});e.setState({isLoading:!1,data:a})},function(t){e.setState({isLoading:!1,error:t.detail})})}},{key:"render",value:function(){return v["default"].createElement("div",{className:"modal-dialog"+(this.state.error?" modal-message":" modal-sm"),role:"document"},v["default"].createElement("div",{className:"modal-content"},v["default"].createElement("div",{className:"modal-header"},v["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},v["default"].createElement("span",{"aria-hidden":"true"},"×")),v["default"].createElement("h4",{className:"modal-title"},gettext("Poll votes"))),v["default"].createElement(s,{data:this.state.data,error:this.state.error,isLoading:this.state.isLoading})))}}]),t}(v["default"].Component);a["default"]=x},{"../../../services/ajax":364,"../../modal-loader":60,"../../modal-message":61,moment:"moment",react:"react"}],111:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e,t,a){return a.is_public||t.can_delete||t.can_edit||t.can_see_votes||t.can_vote&&!e&&(!a.hasSelectedChoices||a.allow_revotes)}function u(e,t){var a="col-xs-6";return 1===e.length&&(a="col-xs-12"),3===e.length&&e[0]===t&&(a="col-xs-12"),a+" col-sm-3 col-md-2"}function c(e){var t=e.poll.acl.can_vote,a=!e.poll.hasSelectedChoices||e.poll.allow_revotes;return t&&a?p["default"].createElement("div",{className:u(e.controls,0)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:e.poll.isBusy,onClick:e.showVoting,type:"button"},gettext("Vote"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Edit=a.SeeVotes=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){var t=e.isPollOver,a=e.poll,n=e.showVoting,r=e.thread;if(!i(t,a.acl,a))return null;var o=[],l=a.acl.can_vote,s=!a.hasSelectedChoices||a.allow_revotes;return l&&s&&o.push(0),(a.is_public||a.acl.can_see_votes)&&o.push(1),a.acl.can_edit&&o.push(2),a.acl.can_delete&&o.push(3),p["default"].createElement("div",{className:"row poll-options"},p["default"].createElement(c,{controls:o,isPollOver:t,poll:a,showVoting:n}),p["default"].createElement(S,{controls:o,poll:a}),p["default"].createElement(M,{controls:o,poll:a,thread:r}),p["default"].createElement(T,{controls:o,poll:a}))},a.isVisible=i,a.getClassName=u,a.ChangeVote=c;var f=e("react"),p=r(f),m=e("./modal"),h=r(m),b=e("../../../reducers/poll"),v=n(b),g=e("../../../reducers/thread"),_=n(g),y=e("../../../services/ajax"),E=r(y),w=e("../../../services/modal"),O=r(w),k=e("../../../services/posting"),N=r(k),x=e("../../../services/snackbar"),P=r(x),j=e("../../../services/store"),C=r(j),S=a.SeeVotes=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){O["default"].show(p["default"].createElement(h["default"],{poll:n.props.poll}))},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){var e=this.props.poll.is_public||this.props.poll.acl.can_see_votes;return e?p["default"].createElement("div",{className:u(this.props.controls,1)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("See votes"))):null}}]),t}(p["default"].Component),M=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){N["default"].open({submit:n.props.poll.api.index,thread:n.props.thread,poll:n.props.poll,mode:"POLL"})},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){return this.props.poll.acl.can_edit?p["default"].createElement("div",{className:u(this.props.controls,2)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("Edit"))):null}}]),t}(p["default"].Component),T=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=confirm(gettext("Are you sure you want to delete this poll? This action is not reversible."));return!!e&&(C["default"].dispatch(v.busy()),void E["default"]["delete"](n.props.poll.api.index).then(n.handleSuccess,n.handleError))},n.handleSuccess=function(e){P["default"].success("Poll has been deleted"),C["default"].dispatch(v.remove()),C["default"].dispatch(_.updateAcl(e))},n.handleError=function(e){P["default"].apiError(e),C["default"].dispatch(v.release())},r=a,l(n,r)}return s(t,e),d(t,[{key:"render",value:function(){return this.props.poll.acl.can_delete?p["default"].createElement("div",{className:u(this.props.controls,3)},p["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},gettext("Delete"))):null}}]),t}(p["default"].Component)},{"../../../reducers/poll":351,"../../../reducers/thread":359,"../../../services/ajax":364,"../../../services/modal":370,"../../../services/posting":374,"../../../services/snackbar":375,"../../../services/store":376,"./modal":110,react:"react"}],112:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.choicesLeft;if(0===t)return s["default"].createElement("li",{className:"poll-help-choices-left"},gettext("You can't select any more choices."));var a=ngettext("You can select %(choices)s more choice.","You can select %(choices)s more choices.",t),n=interpolate(a,{choices:t},!0);return s["default"].createElement("li",{className:"poll-help-choices-left"},n)}function o(e){return e.poll.allow_revotes?s["default"].createElement("li",{className:"poll-help-allow-revotes"},gettext("You can change your vote later.")):s["default"].createElement("li",{className:"poll-help-no-revotes"},gettext("Votes are final."))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return s["default"].createElement("ul",{className:"list-unstyled list-inline poll-help"},s["default"].createElement(r,{choicesLeft:e.choicesLeft}),s["default"].createElement(o,{poll:e.poll}))},a.PollChoicesLeft=r,a.PollAllowRevote=o;var l=e("react"),s=n(l),i=e("../../../utils/escape-html");n(i)},{"../../../utils/escape-html":382,react:"react"}],113:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./help"),f=r(d),p=e("./select"),m=r(p),h=e("./utils"),b=e("../info"),v=r(b),g=e("../results/options"),_=e("../../button"),y=r(_),E=e("../../form"),w=r(E),O=e("../../../reducers/poll"),k=n(O),N=e("../../../services/ajax"),x=r(N),P=e("../../../services/snackbar"),j=r(P),C=e("../../../services/store"),S=r(C),M=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.toggleChoice=function(e){var t=(0,h.getChoiceFromHash)(a.state.choices,e),n=null;n=t.selected?a.deselectChoice(t,e):a.selectChoice(t,e),a.setState({choices:n,choicesLeft:(0,h.getChoicesLeft)(a.props.poll,n)})},a.selectChoice=function(e,t){var n=(0,h.getChoicesLeft)(a.props.poll,a.state.choices);if(!n)for(var r in a.state.choices.slice()){var o=a.state.choices[r];if(o.selected&&o.hash!=t){o.selected=!1;break}}return a.state.choices.map(function(e){return Object.assign({},e,{selected:e.hash==t||e.selected})})},a.deselectChoice=function(e,t){return a.state.choices.map(function(e){return Object.assign({},e,{selected:e.hash!=t&&e.selected})})},a.state={isLoading:!1,choices:e.poll.choices,choicesLeft:(0,h.getChoicesLeft)(e.poll,e.poll.choices)},a}return s(t,e),i(t,[{key:"clean",value:function(){return this.state.choicesLeft!==this.props.poll.allowed_choices||(j["default"].error(gettext("You need to select at least one choice")),!1)}},{key:"send",value:function(){var e=[];for(var t in this.state.choices.slice()){var a=this.state.choices[t];a.selected&&e.push(a.hash)}return x["default"].post(this.props.poll.api.votes,e)}},{key:"handleSuccess",value:function(e){S["default"].dispatch(k.replace(e)),j["default"].success(gettext("Your vote has been saved.")),this.props.showResults()}},{key:"handleError",value:function(e){400===e.status?j["default"].error(e.detail):j["default"].apiError(e)}},{key:"render",value:function(){var e=[];return this.props.poll.acl.can_vote&&e.push(0),(this.props.poll.is_public||this.props.poll.acl.can_see_votes)&&e.push(1),this.props.poll.acl.can_edit&&e.push(2),this.props.poll.acl.can_delete&&e.push(3),c["default"].createElement("div",{className:"panel panel-default panel-poll"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"panel-body"},c["default"].createElement("h2",null,this.props.poll.question),c["default"].createElement(v["default"],{poll:this.props.poll}),c["default"].createElement(m["default"],{choices:this.state.choices,toggleChoice:this.toggleChoice}),c["default"].createElement(f["default"],{choicesLeft:this.state.choicesLeft,poll:this.props.poll})),c["default"].createElement("div",{className:"panel-footer"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:(0,g.getClassName)(e,0)},c["default"].createElement(y["default"],{className:"btn-primary btn-block btn-sm",loading:this.state.isLoading},gettext("Save your vote"))),c["default"].createElement("div",{className:(0,g.getClassName)(e,1)},c["default"].createElement("button",{className:"btn btn-default btn-block btn-sm",disabled:this.state.isLoading,onClick:this.props.showResults,type:"button"},gettext("See results"))),c["default"].createElement(g.Edit,{controls:e,poll:this.props.poll,thread:this.props.thread}),c["default"].createElement(g.Delete,{controls:e,poll:this.props.poll})))))}}]),t}(w["default"]);a["default"]=M},{"../../../reducers/poll":351,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,"../../button":8,"../../form":55,"../info":106,"../results/options":111,"./help":112,"./select":114,"./utils":115,react:"react"}],114:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.ChoiceSelect=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return u["default"].createElement("ul",{className:"list-unstyled poll-select-choices"},e.choices.map(function(t){return u["default"].createElement(c,{choice:t,key:t.hash,toggleChoice:e.toggleChoice})}))};var i=e("react"),u=n(i),c=a.ChoiceSelect=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.toggleChoice(n.props.choice.hash)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("li",{className:"poll-select-choice"},u["default"].createElement("button",{className:this.props.choice.selected?"btn btn-selected":"btn",onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},this.props.choice.selected?"check_box":"check_box_outline_blank"),u["default"].createElement("strong",null,this.props.choice.label)))}}]),t}(u["default"].Component)},{react:"react"}],115:[function(e,t,a){"use strict";function n(e,t){for(var a in e){var n=e[a];if(n.hash===t)return n}return null}function r(e,t){var a=[];for(var n in t){var r=t[n];r.selected&&a.push(r)}return e.allowed_choices-a.length}Object.defineProperty(a,"__esModule",{value:!0}),a.getChoiceFromHash=n,a.getChoicesLeft=r},{}],116:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return"?"===e.item[0]?null:i["default"].createElement("li",{className:o(e.item)},l(e.item))}function o(e){var t="diff-item";return"-"===e[0]?t+=" diff-item-sub":"+"===e[0]&&(t+=" diff-item-add"),t}function l(e){return e.substr(2)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return i["default"].createElement("div",{className:"modal-body post-changelog-diff"},i["default"].createElement("ul",{className:"list-unstyled"},e.diff.map(function(e,t){return i["default"].createElement(r,{item:e,key:t})})))},a.DiffItem=r,a.getItemClassName=o,a.cleanItem=l;var s=e("react"),i=n(s)},{react:"react"}],117:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.revertEdit(n.props.edit.id)},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.canRevert?u["default"].createElement("div",{className:"modal-footer visible-xs-block"},u["default"].createElement(d["default"],{className:"btn-default btn-sm btn-block",disabled:this.props.disabled,onClick:this.onClick,title:gettext("Revert post to state from before this edit.")},gettext("Revert"))):null}}]),t}(u["default"].Component);a["default"]=f},{"../button":8,react:"react"}],118:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:e.className||"modal-dialog",role:"document"},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Post edits history"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalDialog=i;var c=e("react"),d=r(c),f=e("./diff"),p=r(f),m=e("./footer"),h=r(m),b=e("./toolbar"),v=r(b),g=e("./utils"),_=e("../modal-message"),y=r(_),E=e("../modal-loader"),w=r(E),O=e("../../reducers/post"),k=n(O),N=e("../../services/ajax"),x=r(N),P=e("../../services/modal"),j=r(P),C=e("../../services/snackbar"),S=r(C),M=e("../../services/store"),T=r(M),L=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.goToEdit=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;a.setState({isBusy:!0});var t=a.props.post.api.edits;null!==e&&(t+="?edit="+e),x["default"].get(t).then(function(e){a.setState({isReady:!0,isBusy:!1,edit:(0,g.hydrateEdit)(e)})},function(e){a.setState({isReady:!0,isBusy:!1,error:e.detail})})},a.revertEdit=function(e){if(!a.state.isBusy){var t=confirm(gettext("Are you sure you with to revert this post to the state from before this edit?"));if(t){a.setState({isBusy:!0});var n=a.props.post.api.edits+"?edit="+e;x["default"].post(n).then(function(e){var t=k.hydrate(e);T["default"].dispatch(k.patch(e,t)),S["default"].success(gettext("Post has been reverted to previous state.")),j["default"].hide()},function(e){S["default"].apiError(e),a.setState({isBusy:!1})})}}},a.state={isReady:!1,isBusy:!0,canRevert:e.post.acl.can_edit,error:null,edit:null},a}return s(t,e),u(t,[{key:"componentDidMount",value:function(){this.goToEdit()}},{key:"render",value:function(){return this.state.error?d["default"].createElement(i,{className:"modal-dialog modal-message"},d["default"].createElement(y["default"],{message:this.state.error})):this.state.isReady?d["default"].createElement(i,null,d["default"].createElement(v["default"],{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,goToEdit:this.goToEdit,revertEdit:this.revertEdit}),d["default"].createElement(p["default"],{diff:this.state.edit.diff}),d["default"].createElement(h["default"],{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,revertEdit:this.revertEdit})):d["default"].createElement(i,null,d["default"].createElement(w["default"],null))}}]),t}(d["default"].Component);a["default"]=L},{"../../reducers/post":352,"../../services/ajax":364,"../../services/modal":370,"../../services/snackbar":375,"../../services/store":376,"../modal-loader":60,"../modal-message":61,"./diff":116,"./footer":117,"./toolbar":119,"./utils":120,react:"react"}],119:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.previous,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function i(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function u(e){return m["default"].createElement(b["default"],{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:gettext("See previous change")},m["default"].createElement("span",{className:"material-icon"},"last_page"))}function c(e){return e.canRevert?m["default"].createElement("div",{className:"col-sm-3 hidden-xs"},m["default"].createElement(b["default"],{className:"btn-default btn-sm btn-block",disabled:e.disabled,onClick:e.onClick,title:gettext("Revert post to state from before this edit.")},gettext("Revert"))):null}function d(e){var t=null;t=e.edit.url.editor?interpolate(E,{url:(0,g["default"])(e.edit.url.editor),user:(0,g["default"])(e.edit.editor_name)},!0):interpolate(y,{user:(0,g["default"])(e.edit.editor_name)},!0);var a=interpolate(_,{absolute:(0,g["default"])(e.edit.edited_on.format("LLL")),relative:(0,g["default"])(e.edit.edited_on.fromNow())},!0),n=interpolate((0,
+g["default"])(gettext("By %(edited_by)s %(edited_on)s.")),{edited_by:t,edited_on:a},!0);return m["default"].createElement("p",{dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0});var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.GoBackBtn=s,a.GoForwardBtn=i,a.GoLastBtn=u,a.RevertBtn=c,a.Label=d;var p=e("react"),m=n(p),h=e("../button"),b=n(h),v=e("../../utils/escape-html"),g=n(v),_='<abbr title="%(absolute)s">%(relative)s</abbr>',y='<span class="item-title">%(user)s</span>',E='<a href="%(url)s" class="item-title">%(user)s</a>',w=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.goLast=function(){n.props.goToEdit()},n.goForward=function(){n.props.goToEdit(n.props.edit.next)},n.goBack=function(){n.props.goToEdit(n.props.edit.previous)},n.revertEdit=function(){n.props.revertEdit(n.props.edit.id)},l=a,o(n,l)}return l(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("div",{className:"modal-toolbar post-changelog-toolbar"},m["default"].createElement("div",{className:"row"},m["default"].createElement("div",{className:"col-xs-12 col-sm-4"},m["default"].createElement("div",{className:"row"},m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(s,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goBack})),m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(i,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goForward})),m["default"].createElement("div",{className:"col-xs-4"},m["default"].createElement(u,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goLast})))),m["default"].createElement("div",{className:"col-xs-12 col-sm-5 xs-margin-top-half post-change-label"},m["default"].createElement(d,{edit:this.props.edit})),m["default"].createElement(c,{canRevert:this.props.canRevert,disabled:this.props.disabled,onClick:this.revertEdit})))}}]),t}(m["default"].Component);a["default"]=w},{"../../utils/escape-html":382,"../button":8,react:"react"}],120:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{edited_on:(0,l["default"])(e.edited_on)})}Object.defineProperty(a,"__esModule",{value:!0}),a.hydrateEdit=r;var o=e("moment"),l=n(o)},{moment:"moment"}],121:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.isReady,a=e.posts,n=e.poster;return t?o["default"].createElement("ul",{className:"posts-list post-feed ui-ready"},a.map(function(e){return o["default"].createElement(s["default"],{key:e.id,post:e,poster:n})})):o["default"].createElement(u["default"],null)};var r=e("react"),o=n(r),l=e("./post"),s=n(l),i=e("./preview"),u=n(i)},{"./post":124,"./preview":130,react:"react"}],122:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("div",{className:"post-body"},s["default"].createElement(u["default"],{markup:e.post.content}))}function o(e){return s["default"].createElement("div",{className:"post-body post-body-invalid"},s["default"].createElement("p",{className:"lead"},gettext("This post's contents cannot be displayed.")),s["default"].createElement("p",{className:"text-muted"},gettext("This error is caused by invalid post content manipulation.")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.content?s["default"].createElement(r,e):s["default"].createElement(o,e)},a.Default=r,a.Invalid=o;var l=e("react"),s=n(l),i=e("../../misago-markup"),u=n(i),c=e("../../../utils/escape-html");n(c)},{"../../../utils/escape-html":382,"../../misago-markup":59,react:"react"}],123:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=t.category,n=t.thread,r=interpolate(gettext("posted %(posted_on)s"),{posted_on:t.posted_on.format("LL, LT")},!0);return o["default"].createElement("div",{className:"post-heading"},o["default"].createElement("a",{className:"btn btn-link item-title",href:n.url},n.title),o["default"].createElement("a",{className:"btn btn-link post-category",href:a.url.index},a.name),o["default"].createElement("a",{href:t.url.index,className:"btn btn-link posted-on",title:r},t.posted_on.fromNow()))};var r=e("react"),o=n(r)},{react:"react"}],124:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster,n=a||t.poster,r="post";return n&&n.rank.css_class&&(r+=" post-"+n.rank.css_class),o["default"].createElement("li",{className:r,id:"post-"+t.id},o["default"].createElement("div",{className:"panel panel-default panel-post"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement(d["default"],{post:t,poster:n}),o["default"].createElement(u["default"],{post:t}),o["default"].createElement(s["default"],{post:t}))))};var r=e("react"),o=n(r),l=e("./body"),s=n(l),i=e("./header"),u=n(i),c=e("./post-side"),d=n(c)},{"./body":122,"./header":123,"./post-side":127,react:"react"}],125:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return o["default"].createElement("div",{className:"post-side post-side-anonymous"},o["default"].createElement(u["default"],{post:t}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("span",null,o["default"].createElement(s["default"],{className:"poster-avatar",size:50}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("span",{className:"item-title"},t.poster_name)),o["default"].createElement("span",{className:"user-title user-title-anonymous"},gettext("Removed user")))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("./button"),u=n(i)},{"../../../avatar":6,"./button":126,react:"react"}],126:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return o["default"].createElement("a",{className:"btn btn-default btn-icon pull-right",href:t.url.index},o["default"].createElement("span",{className:"btn-text-left hidden-xs"},gettext("See post")),o["default"].createElement("span",{className:"material-icon"},"chevron_right"))};var r=e("react"),o=n(r)},{react:"react"}],127:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster;return a.id?o["default"].createElement(u["default"],{post:t,poster:a}):o["default"].createElement(s["default"],{post:t})};var r=e("react"),o=n(r),l=e("./anonymous"),s=n(l),i=e("./registered"),u=n(i)},{"./anonymous":125,"./registered":128,react:"react"}],128:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.poster;return o["default"].createElement("div",{className:"post-side post-side-registered"},o["default"].createElement(u["default"],{post:t}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{className:"poster-avatar",size:50,user:a}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("a",{className:"item-title",href:a.url},a.username)),o["default"].createElement(d["default"],{title:a.title,rank:a.rank}))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("./button"),u=n(i),c=e("./user-title"),d=n(c)},{"../../../avatar":6,"./button":126,"./user-title":129,react:"react"}],129:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.rank,a=e.title,n=a||t.title||t.name,r="user-title";return t.css_class&&(r+=" user-title-"+t.css_class),t.is_tab?o["default"].createElement("a",{className:r,href:t.url},n):o["default"].createElement("span",{className:r},n)};var r=e("react"),o=n(r)},{react:"react"}],130:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){return l["default"].createElement("ul",{className:"posts-list post-feed ui-preview"},l["default"].createElement("li",{className:"post"},l["default"].createElement("div",{className:"panel panel-default panel-post"},l["default"].createElement("div",{className:"panel-body"},l["default"].createElement("div",{className:"post-side post-side-anonymous"},l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left"},l["default"].createElement("span",null,l["default"].createElement(i["default"],{className:"poster-avatar",size:50}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement("div",{className:"media-heading"},l["default"].createElement("span",{className:"item-title"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))),l["default"].createElement("span",{className:"user-title user-title-anonymous"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))))),l["default"].createElement("div",{className:"post-heading"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")),l["default"].createElement("div",{className:"post-body"},l["default"].createElement("article",{className:"misago-markup"},l["default"].createElement("p",null,l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")," ",l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," ")," ",l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,200)+"px"}}," "))))))))};var o=e("react"),l=r(o),s=e("../avatar"),i=r(s),u=e("../../utils/random"),c=n(u)},{"../../utils/random":387,"../avatar":6,react:"react"}],131:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return Object.assign({},e,{liked_on:(0,v["default"])(e.liked_on)})}function i(e){var t=e.className,a=e.children,n=e.likes,r=gettext("Post Likes");if(n){var o=n.length,l=ngettext("%(likes)s like","%(likes)s likes",o);r=interpolate(l,{likes:o},!0)}return h["default"].createElement("div",{className:"modal-dialog "+(t||""),role:"document"},h["default"].createElement("div",{className:"modal-content"},h["default"].createElement("div",{className:"modal-header"},h["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},h["default"].createElement("span",{"aria-hidden":"true"},"×")),h["default"].createElement("h4",{className:"modal-title"},r)),a))}function u(e){return h["default"].createElement("div",{className:"modal-body modal-post-likers"},h["default"].createElement("ul",{className:"media-list"},e.likes.map(function(e){return h["default"].createElement(c,f({key:e.id},e))})))}function c(e){if(e.url){var t={id:e.liker_id,avatars:e.avatars};return h["default"].createElement("li",{className:"media"},h["default"].createElement("div",{className:"media-left"},h["default"].createElement("a",{className:"user-avatar",href:e.url},h["default"].createElement(_["default"],{size:"50",user:t}))),h["default"].createElement("div",{className:"media-body"},h["default"].createElement("a",{className:"item-title",href:e.url},e.username)," ",h["default"].createElement(d,{likedOn:e.liked_on})))}return h["default"].createElement("li",{className:"media"},h["default"].createElement("div",{className:"media-left"},h["default"].createElement("span",{className:"user-avatar"},h["default"].createElement(_["default"],{size:"50"}))),h["default"].createElement("div",{className:"media-body"},h["default"].createElement("strong",null,e.username)," ",h["default"].createElement(d,{likedOn:e.liked_on})))}function d(e){return h["default"].createElement("span",{className:"text-muted",title:e.likedOn.format("LLL")},e.likedOn.fromNow())}Object.defineProperty(a,"__esModule",{value:!0});var f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},p=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.hydrateLike=s,a.ModalDialog=i,a.LikesList=u,a.LikeDetails=c,a.LikeDate=d;var m=e("react"),h=n(m),b=e("moment"),v=n(b),g=e("./avatar"),_=n(g),y=e("./modal-message"),E=n(y),w=e("./modal-loader"),O=n(w),k=e("../services/ajax"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isReady:!1,error:null,likes:[]},a}return l(t,e),p(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.post.api.likes).then(function(t){e.setState({isReady:!0,likes:t.map(s)})},function(t){e.setState({isReady:!0,error:t.detail})})}},{key:"render",value:function(){return this.state.error?h["default"].createElement(i,{className:"modal-message"},h["default"].createElement(E["default"],{message:this.state.error})):this.state.isReady?this.state.likes.length?h["default"].createElement(i,{className:"modal-sm",likes:this.state.likes},h["default"].createElement(u,{likes:this.state.likes})):h["default"].createElement(i,{className:"modal-message"},h["default"].createElement(E["default"],{message:gettext("No users have liked this post.")})):h["default"].createElement(i,{className:"modal-sm"},h["default"].createElement(O["default"],null))}}]),t}(h["default"].Component);a["default"]=x},{"../services/ajax":364,"./avatar":6,"./modal-loader":60,"./modal-message":61,moment:"moment",react:"react"}],132:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/loader"),g=r(v),_=e("./utils/message"),y=r(_),E=e("./utils/attachments"),w=n(E),O=e("./utils/validators"),k=e("../../services/ajax"),N=r(k),x=e("../../services/posting"),P=r(x),j=e("../../services/snackbar"),C=r(j),S=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){a.setState({isReady:!0,post:e.post,attachments:w.hydrate(e.attachments),protect:e.is_protected,canProtect:e.can_protect})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard changes?"));e&&P["default"].close()},a.onProtect=function(){a.setState({protect:!0})},a.onUnprotect=function(){a.setState({protect:!1})},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})},a.state={isReady:!1,isLoading:!1,isErrored:!1,post:"",attachments:[],protect:!1,canProtect:!1,validators:{post:(0,O.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){N["default"].get(this.props.config).then(this.loadSuccess,this.loadError)}},{key:"clean",value:function(){if(!this.state.post.trim().length)return C["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return!e.post||(C["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return N["default"].put(this.props.submit,{post:this.state.post,attachments:w.clean(this.state.attachments),protect:this.state.protect})}},{key:"handleSuccess",value:function(e){C["default"].success(gettext("Reply has been edited.")),window.location=e.url.index,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);C["default"].error(t[0])}else C["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?c["default"].createElement(b["default"],{className:"posting-form"},c["default"].createElement("form",{onSubmit:this.handleSubmit,method:"POST"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,canProtect:this.state.canProtect,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,onProtect:this.onProtect,onUnprotect:this.onUnprotect,protect:this.state.protect,submitLabel:gettext("Edit reply"),value:this.state.post}))))):this.state.isErrored?c["default"].createElement(y["default"],{message:this.state.isErrored}):c["default"].createElement(g["default"],null)}}]),t}(m["default"]);a["default"]=S},{"../../services/ajax":364,"../../services/posting":374,"../../services/snackbar":375,"../editor":51,"../form":55,"./utils/attachments":137,"./utils/container":138,"./utils/loader":139,"./utils/message":140,"./utils/validators":143,react:"react"}],133:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return"START"===e.mode?o["default"].createElement(s["default"],e):"START_PRIVATE"===e.mode?o["default"].createElement(u["default"],e):"REPLY"===e.mode?o["default"].createElement(d["default"],e):"EDIT"===e.mode?o["default"].createElement(p["default"],e):null};var r=e("react"),o=n(r),l=e("./start"),s=n(l),i=e("./start-private"),u=n(i),c=e("./reply"),d=n(c),f=e("./edit"),p=n(f)},{"./edit":132,"./reply":134,"./start":136,"./start-private":135,react:"react"}],134:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/loader"),g=r(v),_=e("./utils/message"),y=r(_),E=e("./utils/attachments"),w=n(E),O=e("./utils/validators"),k=e("../../services/ajax"),N=r(k),x=e("../../services/posting"),P=r(x),j=e("../../services/snackbar"),C=r(j),S=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){a.setState({isReady:!0,post:e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]":""})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.appendData=function(e){var t=e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]\n\n":"";a.setState(function(e,a){return e.post.length>0?{post:e.post+"\n\n"+t}:{post:t}})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard your reply?"));e&&P["default"].close()},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})},a.state={isReady:!1,isLoading:!1,isErrored:!1,post:"",attachments:[],validators:{post:(0,O.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){N["default"].get(this.props.config,this.props.context||null).then(this.loadSuccess,this.loadError)}},{key:"componentWillReceiveProps",value:function(e){var t=this.props.context,a=e.context;t&&a&&t.reply===a.reply||N["default"].get(e.config,e.context||null).then(this.appendData,C["default"].apiError)}},{key:"clean",value:function(){if(!this.state.post.trim().length)return C["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return!e.post||(C["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return N["default"].post(this.props.submit,{post:this.state.post,attachments:w.clean(this.state.attachments)})}},{key:"handleSuccess",value:function(e){C["default"].success(gettext("Your reply has been posted.")),window.location=e.url.index,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.post||[],e.attachments||[]);C["default"].error(t[0])}else C["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?c["default"].createElement(b["default"],{className:"posting-form"},c["default"].createElement("form",{onSubmit:this.handleSubmit,method:"POST"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post reply"),value:this.state.post}))))):this.state.isErrored?c["default"].createElement(y["default"],{message:this.state.isErrored}):c["default"].createElement(g["default"],null)}}]),t}(m["default"]);a["default"]=S},{"../../services/ajax":364,"../../services/posting":374,"../../services/snackbar":375,"../editor":51,"../form":55,"./utils/attachments":137,"./utils/container":138,"./utils/loader":139,"./utils/message":140,"./utils/validators":143,react:"react"}],135:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../editor"),f=r(d),p=e("../form"),m=r(p),h=e("./utils/container"),b=r(h),v=e("./utils/message"),g=(r(v),e("./utils/attachments")),_=n(g),y=e("./utils/usernames"),E=r(y),w=e("./utils/validators"),O=e("../../services/ajax"),k=r(O),N=e("../../services/posting"),x=r(N),P=e("../../services/snackbar"),j=r(P),C=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard private thread?"));e&&x["default"].close()},a.onToChange=function(e){a.changeValue("to",e.target.value)},a.onTitleChange=function(e){a.changeValue("title",e.target.value)},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){a.setState({attachments:e})};var n=(e.to||[]).map(function(e){return e.username}).join(", ");return a.state={isLoading:!1,to:n,title:"",post:"",attachments:[],validators:{title:(0,w.getTitleValidators)(),post:(0,w.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"clean",value:function(){if(!(0,E["default"])(this.state.to).length)return j["default"].error(gettext("You have to enter at least one recipient.")),!1;if(!this.state.title.trim().length)return j["default"].error(gettext("You have to enter thread title.")),!1;if(!this.state.post.trim().length)return j["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return e.title?(j["default"].error(e.title[0]),!1):!e.post||(j["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.submit,{to:(0,E["default"])(this.state.to),title:this.state.title,post:this.state.post,attachments:_.clean(this.state.attachments)})}},{key:"handleSuccess",value:function(e){j["default"].success(gettext("Your thread has been posted.")),window.location=e.url,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.to||[],e.title||[],e.post||[],e.attachments||[]);j["default"].error(t[0])}else j["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement(b["default"],{className:"posting-form",withFirstRow:!0},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onToChange,placeholder:gettext("Comma separated list of user names, eg.: Danny, Lisa"),type:"text",value:this.state.to}))),c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:gettext("Thread title"),type:"text",value:this.state.title}))),c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-xs-12"},c["default"].createElement(f["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post thread"),value:this.state.post})))))}}]),t}(m["default"]);a["default"]=C},{"../../services/ajax":364,"../../services/posting":374,"../../services/snackbar":375,"../editor":51,"../form":55,"./utils/attachments":137,"./utils/container":138,"./utils/message":140,"./utils/usernames":142,"./utils/validators":143,react:"react"}],136:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../category-select"),f=r(d),p=e("../editor"),m=r(p),h=e("../form"),b=r(h),v=e("./utils/container"),g=r(v),_=e("./utils/loader"),y=r(_),E=e("./utils/message"),w=r(E),O=e("./utils/options"),k=r(O),N=e("./utils/attachments"),x=n(N),P=e("./utils/validators"),j=e("../../services/ajax"),C=r(j),S=e("../../services/posting"),M=r(S),T=e("../../services/snackbar"),L=r(T),A=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadSuccess=function(e){var t=null,n=!1,r=null,o=e.map(function(e){return e.post===!1||t&&e.id!=a.state.category||(t=e.id,r=e.post),e.post&&(e.post.close||e.post.hide||e.post.pin)&&(n=!0),Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id})});a.setState({isReady:!0,showOptions:n,categories:o,category:t,categoryOptions:r})},a.loadError=function(e){a.setState({isErrored:e.detail})},a.onCancel=function(){var e=confirm(gettext("Are you sure you want to discard thread?"));e&&M["default"].close()},a.onTitleChange=function(e){a.changeValue("title",e.target.value)},a.onCategoryChange=function(e){var t=a.state.categories.find(function(t){return e.target.value==t.value}),n=a.state.pin;t.post.pin&&t.post.pin<n&&(n=t.post.pin),a.setState({category:t.id,categoryOptions:t.post,pin:n})},a.onPostChange=function(e){a.changeValue("post",e.target.value)},a.onAttachmentsChange=function(e){
+a.setState({attachments:e})},a.onClose=function(){a.changeValue("close",!0)},a.onOpen=function(){a.changeValue("close",!1)},a.onPinGlobally=function(){a.changeValue("pin",2)},a.onPinLocally=function(){a.changeValue("pin",1)},a.onUnpin=function(){a.changeValue("pin",0)},a.onHide=function(){a.changeValue("hide",!0)},a.onUnhide=function(){a.changeValue("hide",!1)},a.state={isReady:!1,isLoading:!1,isErrored:!1,showOptions:!1,categoryOptions:null,title:"",category:e.category||null,categories:[],post:"",attachments:[],close:!1,hide:!1,pin:0,validators:{title:(0,P.getTitleValidators)(),post:(0,P.getPostValidators)()},errors:{}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){C["default"].get(this.props.config).then(this.loadSuccess,this.loadError)}},{key:"clean",value:function(){if(!this.state.title.trim().length)return L["default"].error(gettext("You have to enter thread title.")),!1;if(!this.state.post.trim().length)return L["default"].error(gettext("You have to enter a message.")),!1;var e=this.validate();return e.title?(L["default"].error(e.title[0]),!1):!e.post||(L["default"].error(e.post[0]),!1)}},{key:"send",value:function(){return C["default"].post(this.props.submit,{title:this.state.title,category:this.state.category,post:this.state.post,attachments:x.clean(this.state.attachments),close:this.state.close,hide:this.state.hide,pin:this.state.pin})}},{key:"handleSuccess",value:function(e){L["default"].success(gettext("Your thread has been posted.")),window.location=e.url,this.setState({isLoading:!0})}},{key:"handleError",value:function(e){if(400===e.status){var t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);L["default"].error(t[0])}else L["default"].apiError(e)}},{key:"render",value:function(){if(this.state.isErrored)return c["default"].createElement(w["default"],{message:this.state.isErrored});if(!this.state.isReady)return c["default"].createElement(y["default"],null);var e=0;this.state.categoryOptions.close&&(e+=1),this.state.categoryOptions.hide&&(e+=1),this.state.categoryOptions.pin&&(e+=1);var t=null;return t=1===e?"col-sm-6":"col-sm-8",t+=3===e?" col-md-6":e?" col-md-7":" col-md-9",c["default"].createElement(g["default"],{className:"posting-form",withFirstRow:!0},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"row first-row"},c["default"].createElement("div",{className:t},c["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:gettext("Thread title"),type:"text",value:this.state.title})),c["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3 xs-margin-top"},c["default"].createElement(f["default"],{choices:this.state.categories,disabled:this.state.isLoading,onChange:this.onCategoryChange,value:this.state.category})),c["default"].createElement(k["default"],{close:this.state.close,columns:e,disabled:this.state.isLoading,hide:this.state.hide,onClose:this.onClose,onHide:this.onHide,onOpen:this.onOpen,onPinGlobally:this.onPinGlobally,onPinLocally:this.onPinLocally,onUnhide:this.onUnhide,onUnpin:this.onUnpin,options:this.state.categoryOptions,pin:this.state.pin,showOptions:this.state.showOptions})),c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-md-12"},c["default"].createElement(m["default"],{attachments:this.state.attachments,loading:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onCancel:this.onCancel,onChange:this.onPostChange,submitLabel:gettext("Post thread"),value:this.state.post})))))}}]),t}(b["default"]);a["default"]=A},{"../../services/ajax":364,"../../services/posting":374,"../../services/snackbar":375,"../category-select":21,"../editor":51,"../form":55,"./utils/attachments":137,"./utils/container":138,"./utils/loader":139,"./utils/message":140,"./utils/options":141,"./utils/validators":143,react:"react"}],137:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.filter(function(e){return e.id&&!e.isRemoved});return t.map(function(e){return e.id})}function o(e){return e.map(function(e){return Object.assign({},e,{uploaded_on:(0,s["default"])(e.uploaded_on)})})}Object.defineProperty(a,"__esModule",{value:!0}),a.clean=r,a.hydrate=o;var l=e("moment"),s=n(l)},{moment:"moment"}],138:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:e.className},o["default"].createElement("div",{className:"container"},e.children))};var r=e("react"),o=n(r)},{react:"react"}],139:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement(s["default"],{className:"posting-loader"},o["default"].createElement(u["default"],null))};var r=e("react"),o=n(r),l=e("./container"),s=n(l),i=e("../../loader"),u=n(i)},{"../../loader":57,"./container":138,react:"react"}],140:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement(s["default"],{className:"posting-message"},o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",null,o["default"].createElement("span",{className:"material-icon"},"error_outline"),e.message),o["default"].createElement("button",{type:"button",className:"btn btn-default",onClick:u["default"].close},gettext("Dismiss"))))};var r=e("react"),o=n(r),l=e("./container"),s=n(l),i=e("../../../services/posting"),u=n(i)},{"../../../services/posting":374,"./container":138,react:"react"}],141:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(!e.show)return null;var t=e.close?gettext("Closed"):gettext("Open");return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:e.close?e.onOpen:e.onClose,title:t,type:"button"},i["default"].createElement("span",{className:"material-icon"},e.close?"lock":"lock_outline"),i["default"].createElement("span",{className:e.textClassName},t)))}function o(e){if(!e.show)return null;var t=e.hide?gettext("Hidden"):gettext("Not hidden");return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:e.hide?e.onUnhide:e.onHide,title:t,type:"button"},i["default"].createElement("span",{className:"material-icon"},e.hide?"visibility_off":"visibility"),i["default"].createElement("span",{className:e.textClassName},t)))}function l(e){if(!e.show)return null;var t=null,a=null,n=null;switch(e.pin){case 0:t="radio_button_unchecked",a=e.onPinLocally,n=gettext("Unpinned");break;case 1:t="bookmark_outline",a=e.onPinGlobally,n=gettext("Pinned locally"),a=2==e.show?e.onPinGlobally:e.onUnpin;break;case 2:t="bookmark",a=e.onUnpin,n=gettext("Pinned globally")}return i["default"].createElement("div",{className:e.className},i["default"].createElement("button",{className:"btn btn-default btn-block",disabled:e.disabled,onClick:a,title:n,type:"button"},i["default"].createElement("span",{className:"material-icon"},t),i["default"].createElement("span",{className:e.textClassName},n)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){if(!e.showOptions)return null;var t=e.columns,a="col-xs-12 xs-margin-top";a+=1===t?" col-sm-2":" sm-margin-top",a+=3===t?" col-md-3":" col-md-2",a+=" posting-options";var n="col-xs-"+12/t,s="btn-text";return s+=3===t?" visible-sm-inline-block":2===t?" hidden-md hidden-lg":" hidden-sm",i["default"].createElement("div",{className:a},i["default"].createElement("div",{className:"row"},i["default"].createElement(l,{className:n,disabled:e.disabled,onPinGlobally:e.onPinGlobally,onPinLocally:e.onPinLocally,onUnpin:e.onUnpin,pin:e.pin,show:e.options.pin,textClassName:s}),i["default"].createElement(o,{className:n,disabled:e.disabled,hide:e.hide,onHide:e.onHide,onUnhide:e.onUnhide,show:e.options.hide,textClassName:s}),i["default"].createElement(r,{className:n,close:e.close,disabled:e.disabled,onClose:e.onClose,onOpen:e.onOpen,show:e.options.close,textClassName:s})))},a.CloseOptions=r,a.HideOptions=o,a.PinOptions=l;var s=e("react"),i=n(s)},{react:"react"}],142:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.split(",").map(function(e){return e.trim().toLowerCase()}),a=t.filter(function(e){return e.length>0}),n=a.filter(function(e,t){return a.indexOf(e)==t});return n}},{}],143:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){return[l(),s()]}function o(){return f["default"].get("SETTINGS").post_length_max?[i(),u()]:[i()]}function l(){return(0,c.minLength)(f["default"].get("SETTINGS").thread_title_length_min,function(e,t){var a=ngettext("Thread title should be at least %(limit_value)s character long (it has %(show_value)s).","Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function s(){return(0,c.maxLength)(f["default"].get("SETTINGS").thread_title_length_max,function(e,t){var a=ngettext("Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).","Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function i(){return(0,c.minLength)(f["default"].get("SETTINGS").post_length_min,function(e,t){var a=ngettext("Posted message should be at least %(limit_value)s character long (it has %(show_value)s).","Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}function u(){return(0,c.maxLength)(f["default"].get("SETTINGS").post_length_max||1e6,function(e,t){var a=ngettext("Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).","Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(a,{limit_value:e,show_value:t},!0)})}Object.defineProperty(a,"__esModule",{value:!0}),a.getTitleValidators=r,a.getPostValidators=o,a.getTitleLengthMin=l,a.getTitleLengthMax=s,a.validatePostLengthMin=i,a.validatePostLengthMax=u;var c=e("../../../utils/validators"),d=e("../../.."),f=n(d)},{"../../..":301,"../../../utils/validators":392}],144:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.can_hide}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=void 0;var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return i(e.post.acl)?d["default"].createElement("li",{className:"event-controls"},d["default"].createElement(w,e),d["default"].createElement(O,e),d["default"].createElement(k,e)):null},a.isVisible=i;var c=e("react"),d=r(c),f=e("moment"),p=r(f),m=e("../../../reducers/post"),h=n(m),b=e("../../../services/ajax"),v=r(b),g=e("../../../services/snackbar"),_=r(g),y=e("../../../services/store"),E=r(y),w=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].dispatch(h.patch(n.props.post,{is_hidden:!0,hidden_on:(0,p["default"])(),hidden_by_name:n.props.user.username,url:Object.assign(n.props.post.url,{hidden_by:n.props.user.url})}));var e={op:"replace",path:"is-hidden",value:!0};v["default"].patch(n.props.post.api.index,[e]).then(function(e){E["default"].dispatch(h.patch(n.props.post,e))},function(e){400===e.status?_["default"].error(e.detail[0]):_["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{is_hidden:!1}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.post.is_hidden?null:d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Hide"))}}]),t}(d["default"].Component),O=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].dispatch(h.patch(n.props.post,{is_hidden:!1}));var e={op:"replace",path:"is-hidden",value:!1};v["default"].patch(n.props.post.api.index,[e]).then(function(e){E["default"].dispatch(h.patch(n.props.post,e))},function(e){400===e.status?_["default"].error(e.detail[0]):_["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{is_hidden:!0}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.post.is_hidden?d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Unhide")):null}}]),t}(d["default"].Component),k=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=confirm(gettext("Are you sure you wish to delete this event? This action is not reversible!"));e&&n["delete"]()},n["delete"]=function(){E["default"].dispatch(h.patch(n.props.post,{isDeleted:!0})),v["default"]["delete"](n.props.post.api.index).then(function(){_["default"].success(gettext("Event has been deleted."))},function(e){400===e.status?_["default"].error(e.detail[0]):_["default"].apiError(e),E["default"].dispatch(h.patch(n.props.post,{isDeleted:!1}))})},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},gettext("Delete"))}}]),t}(d["default"].Component)},{"../../../reducers/post":352,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,moment:"moment",react:"react"}],145:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"post-avatar"},o["default"].createElement("span",{className:"material-icon"},l[e.post.event_type]))};var r=e("react"),o=n(r),l={changed_title:"edit",pinned_globally:"bookmark",pinned_locally:"bookmark_border",unpinned:"panorama_fish_eye",moved:"arrow_forward",merged:"call_merge",approved:"done",opened:"lock_open",closed:"lock_outline",unhid:"visibility",hid:"visibility_off",changed_owner:"grade",tookover:"grade",added_participant:"person_add",owner_left:"person_outline",participant_left:"person_outline",removed_participant:"remove_circle_outline"}},{react:"react"}],146:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t="event";return e.post.isDeleted?t="hide":e.post.is_hidden&&(t="event post-hidden"),o["default"].createElement("li",{id:"post-"+e.post.id,className:t},o["default"].createElement(p["default"],{post:e.post}),o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-2 col-sm-3 text-right"},o["default"].createElement(s["default"],e)),o["default"].createElement("div",{className:"col-xs-10 col-sm-9 text-left"},o["default"].createElement(h["default"],{post:e.post},o["default"].createElement(d["default"],e),o["default"].createElement(u["default"],e)))))};var r=e("react"),o=n(r),l=e("./icon"),s=n(l),i=e("./info"),u=n(i),c=e("./message"),d=n(c),f=e("./unread-label"),p=n(f),m=e("../waypoint"),h=n(m)},{"../waypoint":172,"./icon":145,"./info":147,"./message":148,"./unread-label":149,react:"react"}],147:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(e.post.is_hidden){var t=null;t=e.post.url.hidden_by?interpolate(h,{url:(0,u["default"])(e.post.url.hidden_by),user:(0,u["default"])(e.post.hidden_by_name)},!0):interpolate(m,{user:(0,u["default"])(e.post.hidden_by_name)},!0);var a=interpolate(f,{absolute:(0,u["default"])(e.post.hidden_on.format("LLL")),relative:(0,u["default"])(e.post.hidden_on.fromNow())},!0),n=interpolate((0,u["default"])(gettext("Hidden by %(event_by)s %(event_on)s.")),{event_by:t,event_on:a},!0);return s["default"].createElement("li",{className:"event-hidden-message",dangerouslySetInnerHTML:{__html:n}})}return null}function o(e){var t=null;t=e.post.poster?interpolate(h,{url:(0,u["default"])(e.post.poster.url),user:(0,u["default"])(e.post.poster_name)},!0):interpolate(m,{user:(0,u["default"])(e.post.poster_name)},!0);var a=interpolate(p,{url:(0,u["default"])(e.post.url.index),absolute:(0,u["default"])(e.post.posted_on.format("LLL")),relative:(0,u["default"])(e.post.posted_on.fromNow())},!0),n=interpolate((0,u["default"])(gettext("By %(event_by)s %(event_on)s.")),{event_by:t,event_on:a},!0);return s["default"].createElement("li",{className:"event-posters",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return s["default"].createElement("ul",{className:"list-inline event-info"},s["default"].createElement(r,e),s["default"].createElement(o,e),s["default"].createElement(d["default"],e))},a.Hidden=r,a.Poster=o;var l=e("react"),s=n(l),i=e("../../../utils/escape-html"),u=n(i),c=e("./controls"),d=n(c),f='<abbr title="%(absolute)s">%(relative)s</abbr>',p='<a href="%(url)s" title="%(absolute)s">%(relative)s</a>',m='<span class="item-title">%(user)s</span>',h='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../../utils/escape-html":382,"./controls":144,react:"react"}],148:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=(0,p["default"])(gettext("Thread title has been changed from %(old_title)s.")),a=interpolate(b,{name:(0,p["default"])(e.post.event_context.old_title)},!0),n=interpolate(t,{old_title:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function o(e){var t=(0,p["default"])(gettext("Thread has been moved from %(from_category)s.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.from_category.url),name:(0,p["default"])(e.post.event_context.from_category.name)},!0),n=interpolate(t,{from_category:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function l(e){var t=(0,p["default"])(gettext("The %(merged_thread)s thread has been merged into this thread.")),a=interpolate(b,{name:(0,p["default"])(e.post.event_context.merged_thread)},!0),n=interpolate(t,{merged_thread:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function s(e){var t=(0,p["default"])(gettext("Changed thread owner to %(user)s.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function i(e){var t=(0,p["default"])(gettext("Added %(user)s to thread.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}function u(e){var t=(0,p["default"])(gettext("Removed %(user)s from thread.")),a=interpolate(h,{url:(0,p["default"])(e.post.event_context.user.url),name:(0,p["default"])(e.post.event_context.user.username)},!0),n=interpolate(t,{user:a},!0);return d["default"].createElement("p",{className:"event-message",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return m[e.post.event_type]?d["default"].createElement("p",{className:"event-message"},m[e.post.event_type]):"changed_title"===e.post.event_type?d["default"].createElement(r,e):"moved"===e.post.event_type?d["default"].createElement(o,e):"merged"===e.post.event_type?d["default"].createElement(l,e):"changed_owner"===e.post.event_type?d["default"].createElement(s,e):"added_participant"===e.post.event_type?d["default"].createElement(i,e):"removed_participant"===e.post.event_type?d["default"].createElement(u,e):null},a.ChangedTitle=r,a.Moved=o,a.Merged=l,a.ChangedOwner=s,a.AddedParticipant=i,a.RemovedParticipant=u;var c=e("react"),d=n(c),f=e("../../../utils/escape-html"),p=n(f),m={pinned_globally:gettext("Thread has been pinned globally."),pinned_locally:gettext("Thread has been pinned locally."),unpinned:gettext("Thread has been unpinned."),approved:gettext("Thread has been approved."),opened:gettext("Thread has been opened."),closed:gettext("Thread has been closed."),unhid:gettext("Thread has been revealed."),hid:gettext("Thread has been made hidden."),tookover:gettext("Took thread over."),owner_left:gettext("Owner has left thread. This thread is now closed."),participant_left:gettext("Participant has left thread.")},h='<a href="%(url)s" class="item-title">%(name)s</a>',b='<span class="item-title">%(name)s</span>'},{"../../../utils/escape-html":382,react:"react"}],149:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post;return t.is_read?null:o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-10 col-xs-offset-2 col-sm-9 col-sm-offset-3 text-left"},o["default"].createElement("div",{className:"event-label"},o["default"].createElement("span",{className:"label label-unread"},gettext("New event")))))};var r=e("react"),o=n(r)},{react:"react"}],150:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.post.is_event?s["default"].createElement(u["default"],e):s["default"].createElement(d["default"],e)}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return e.posts.isLoaded?s["default"].createElement("ul",{className:"posts-list ui-ready"},e.posts.results.map(function(t){return s["default"].createElement(r,o({key:t.id,post:t},e))})):s["default"].createElement("ul",{className:"posts-list ui-preview"},s["default"].createElement(p["default"],null))},a.ListItem=r;var l=e("react"),s=n(l),i=e("./event"),u=n(i),c=e("./post"),d=n(c),f=e("./post/preview"),p=n(f)},{"./event":146,"./post":162,"./post/preview":170,react:"react"}],151:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.attachment.is_image?u["default"].createElement("div",{className:"post-attachment-preview"},u["default"].createElement(l,e)):u["default"].createElement("div",{className:"post-attachment-preview"},u["default"].createElement(o,e))}function o(e){return u["default"].createElement("a",{href:e.attachment.url.index,className:"material-icon"},"insert_drive_file")}function l(e){var t=e.attachment.url.thumb||e.attachment.url.index;return u["default"].createElement("a",{className:"post-thumbnail",href:e.attachment.url.index,style:{backgroundImage:'url("'+(0,f["default"])(t)+'")'}})}function s(e){var t=null;t=e.attachment.url.uploader?interpolate(v,{url:(0,f["default"])(e.attachment.url.uploader),user:(0,f["default"])(e.attachment.uploader_name)},!0):interpolate(b,{user:(0,f["default"])(e.attachment.uploader_name)},!0);var a=interpolate(h,{absolute:(0,f["default"])(e.attachment.uploaded_on.format("LLL")),relative:(0,f["default"])(e.attachment.uploaded_on.fromNow())},!0),n=interpolate((0,f["default"])(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")),{filetype:e.attachment.filetype,size:(0,m["default"])(e.attachment.size),uploader:t,uploaded_on:a},!0);return u["default"].createElement("p",{className:"post-attachment-description",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return u["default"].createElement("div",{className:"col-xs-12 col-md-6"},u["default"].createElement(r,e),u["default"].createElement("div",{className:"post-attachment"},u["default"].createElement("a",{href:e.attachment.url.index,className:"attachment-name item-title"},e.attachment.filename),u["default"].createElement(s,e)))},a.AttachmentPreview=r,a.AttachmentIcon=o,a.AttachmentThumbnail=l,a.AttachmentDetails=s;var i=e("react"),u=n(i),c=e("../../../.."),d=(n(c),e("../../../../utils/escape-html")),f=n(d),p=e("../../../../utils/file-size"),m=n(p),h='<abbr title="%(absolute)s">%(relative)s</abbr>',b='<span class="item-title">%(user)s</span>',v='<a href="%(url)s" class="item-title">%(user)s</a>'},{"../../../..":301,"../../../../utils/escape-html":382,"../../../../utils/file-size":383,react:"react"}],152:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return(!e.is_hidden||e.acl.can_see_hidden)&&e.attachments}function o(e){return s["default"].createElement("div",{className:"row"},e.row.map(function(e){return s["default"].createElement(d["default"],{attachment:e,key:e?e.id:0})}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return r(e.post)?s["default"].createElement("div",{className:"post-attachments"},(0,u["default"])(e.post.attachments,2).map(function(e){var t=e.map(function(e){return e?e.id:0}).join("_");return s["default"].createElement(o,{key:t,row:e})})):null},a.isVisible=r,a.Row=o;var l=e("react"),s=n(l),i=e("../../../../utils/batch"),u=n(i),c=e("./attachment"),d=n(c)},{"../../../../utils/batch":379,"./attachment":151,react:"react"}],153:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return i["default"].createElement(c["default"],{className:"post-body",post:e.post},i["default"].createElement(f["default"],{markup:e.post.content}))}function o(e){var t=null;t=e.post.hidden_by?interpolate(h,{url:(0,m["default"])(e.post.url.hidden_by),user:(0,m["default"])(e.post.hidden_by_name)},!0):interpolate(b,{user:(0,m["default"])(e.post.hidden_by_name)},!0);var a=interpolate(v,{absolute:(0,m["default"])(e.post.hidden_on.format("LLL")),relative:(0,m["default"])(e.post.hidden_on.fromNow())},!0),n=interpolate((0,m["default"])(gettext("Hidden by %(hidden_by)s %(hidden_on)s.")),{hidden_by:t,hidden_on:a},!0);return i["default"].createElement(c["default"],{className:"post-body post-body-hidden",post:e.post},i["default"].createElement("p",{className:"lead"},gettext("This post is hidden. You cannot see its contents.")),i["default"].createElement("p",{className:"text-muted",dangerouslySetInnerHTML:{__html:n}}))}function l(e){return i["default"].createElement(c["default"],{className:"post-body post-body-invalid",post:e.post},i["default"].createElement("p",{className:"lead"},gettext("This post's contents cannot be displayed.")),i["default"].createElement("p",{className:"text-muted"},gettext("This error is caused by invalid post content manipulation.")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.is_hidden&&!e.post.acl.can_see_hidden?i["default"].createElement(o,e):e.post.content?i["default"].createElement(r,e):i["default"].createElement(l,e)},a.Default=r,a.Hidden=o,a.Invalid=l;var s=e("react"),i=n(s),u=e("../waypoint"),c=n(u),d=e("../../misago-markup"),f=n(d),p=e("../../../utils/escape-html"),m=n(p),h='<a href="%(url)s" class="item-title">%(user)s</a>',b='<span class="item-title">%(user)s</span>',v='<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'},{"../../../utils/escape-html":382,"../../misago-markup":59,"../waypoint":172,react:"react"}],154:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){j["default"].dispatch(w.patch(e.post,{is_unapproved:!1}));var t=[{op:"replace",path:"is-unapproved",value:!1}],a={is_unapproved:e.post.is_unapproved};f(e,t,a)}function l(e){j["default"].dispatch(w.patch(e.post,{is_protected:!0}));var t=[{op:"replace",path:"is-protected",value:!0}],a={is_protected:e.post.is_protected};f(e,t,a)}function s(e){j["default"].dispatch(w.patch(e.post,{is_protected:!1}));var t=[{op:"replace",path:"is-protected",value:!1}],a={is_protected:e.post.is_protected};f(e,t,a)}function i(e){j["default"].dispatch(w.patch(e.post,{is_hidden:!0,hidden_on:(0,g["default"])(),hidden_by_name:e.user.username,url:Object.assign(e.post.url,{hidden_by:e.user.url})}));var t=[{op:"replace",path:"is-hidden",value:!0}],a={is_hidden:e.post.is_hidden,hidden_on:e.post.hidden_on,hidden_by_name:e.post.hidden_by_name,url:e.post.url};f(e,t,a)}function u(e){j["default"].dispatch(w.patch(e.post,{is_hidden:!1}));var t=[{op:"replace",path:"is-hidden",value:!1}],a={is_hidden:e.post.is_hidden};f(e,t,a)}function c(e){var t=e.post.last_likes||[],a=[e.user].concat(t),n=a.length>3?a.slice(0,-1):a;j["default"].dispatch(w.patch(e.post,{is_liked:!0,likes:e.post.likes+1,last_likes:n}));var r=[{op:"replace",path:"is-liked",value:!0}],o={is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes};f(e,r,o)}function d(e){j["default"].dispatch(w.patch(e.post,{is_liked:!1,likes:e.post.likes-1,last_likes:e.post.last_likes.filter(function(t){return!t.id||t.id!==e.user.id})}));var t=[{op:"replace",path:"is-liked",value:!1}],a={is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes};f(e,t,a)}function f(e,t,a){k["default"].patch(e.post.api.index,t).then(function(t){j["default"].dispatch(w.patch(e.post,t))},function(t){400===t.status?x["default"].error(t.detail[0]):x["default"].apiError(t),j["default"].dispatch(w.patch(e.post,a))})}function p(e){var t=confirm(gettext("Are you sure you want to delete this post? This action is not reversible!"));t&&(j["default"].dispatch(w.patch(e.post,{isDeleted:!0})),k["default"]["delete"](e.post.api.index).then(function(){x["default"].success(gettext("Post has been deleted."))},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),j["default"].dispatch(w.patch(e.post,{isDeleted:!1}))}))}function m(e){var t=e.post,a=e.user;j["default"].dispatch(y.update({best_answer:t.id,best_answer_is_protected:t.is_protected,best_answer_marked_on:(0,g["default"])(),best_answer_marked_by:a.id,best_answer_marked_by_name:a.username,best_answer_marked_by_slug:a.slug
+}));var n=[{op:"replace",path:"best-answer",value:t.id},{op:"add",path:"acl",value:!0}],r={best_answer:e.thread.best_answer,best_answer_is_protected:e.thread.best_answer_is_protected,best_answer_marked_on:e.thread.best_answer_marked_on,best_answer_marked_by:e.thread.best_answer_marked_by,best_answer_marked_by_name:e.thread.best_answer_marked_by_name,best_answer_marked_by_slug:e.thread.best_answer_marked_by_slug};b(e,n,r)}function h(e){var t=e.post;j["default"].dispatch(y.update({best_answer:null,best_answer_is_protected:!1,best_answer_marked_on:null,best_answer_marked_by:null,best_answer_marked_by_name:null,best_answer_marked_by_slug:null}));var a=[{op:"remove",path:"best-answer",value:t.id},{op:"add",path:"acl",value:!0}],n={best_answer:e.thread.best_answer,best_answer_is_protected:e.thread.best_answer_is_protected,best_answer_marked_on:e.thread.best_answer_marked_on,best_answer_marked_by:e.thread.best_answer_marked_by,best_answer_marked_by_name:e.thread.best_answer_marked_by_name,best_answer_marked_by_slug:e.thread.best_answer_marked_by_slug};b(e,a,n)}function b(e,t,a){k["default"].patch(e.thread.api.index,t).then(function(e){e.best_answer_marked_on&&(e.best_answer_marked_on=(0,g["default"])(e.best_answer_marked_on)),j["default"].dispatch(y.update(e))},function(e){400===e.status?x["default"].error(e.detail[0]):x["default"].apiError(e),j["default"].dispatch(y.update(a))})}Object.defineProperty(a,"__esModule",{value:!0}),a.approve=o,a.protect=l,a.unprotect=s,a.hide=i,a.unhide=u,a.like=c,a.unlike=d,a.patch=f,a.remove=p,a.markAsBestAnswer=m,a.unmarkBestAnswer=h,a.patchThread=b;var v=e("moment"),g=r(v),_=e("../../../../reducers/thread"),y=n(_),E=e("../../../../reducers/post"),w=n(E),O=e("../../../../services/ajax"),k=r(O),N=e("../../../../services/snackbar"),x=r(N),P=e("../../../../services/store"),j=r(P)},{"../../../../reducers/post":352,"../../../../reducers/thread":359,"../../../../services/ajax":364,"../../../../services/snackbar":375,"../../../../services/store":376,moment:"moment"}],155:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=a.Unprotect=a.Protect=a.Split=a.Move=a.Approve=a.PostEdits=a.UnmarkMarkBestAnswer=a.MarkAsBestAnswer=a.Edit=a.Permalink=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return c["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},c["default"].createElement(O,e),c["default"].createElement(k,e),c["default"].createElement(N,e),c["default"].createElement(x,e),c["default"].createElement(P,e),c["default"].createElement(j,e),c["default"].createElement(C,e),c["default"].createElement(S,e),c["default"].createElement(M,e),c["default"].createElement(T,e),c["default"].createElement(L,e),c["default"].createElement(A,e),c["default"].createElement(R,e))};var u=e("react"),c=r(u),d=e("../../../../services/modal"),f=r(d),p=e("../../../../services/posting"),m=r(p),h=e("./actions"),b=n(h),v=e("./move"),g=r(v),_=e("../../../post-changelog"),y=r(_),E=e("./split"),w=r(E),O=a.Permalink=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){var e=window.location.protocol+"//";e+=window.location.host,e+=n.props.post.url.index,prompt(gettext("Permament link to this post:"),e)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"link"),gettext("Permament link")))}}]),t}(c["default"].Component),k=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m["default"].open({mode:"EDIT",config:n.props.post.api.editor,submit:n.props.post.api.index})},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_edit?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"edit"),gettext("Edit"))):null}}]),t}(c["default"].Component),N=a.MarkAsBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?t.id===a.best_answer?null:a.best_answer&&!a.acl.can_change_best_answer?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Mark as best answer"))):null}}]),t}(c["default"].Component),x=a.UnmarkMarkBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unmarkBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id!==a.best_answer?null:a.acl.can_unmark_best_answer?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"check_box_outline_blank"),gettext("Unmark best answer"))):null}}]),t}(c["default"].Component),P=a.PostEdits=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(y["default"],{post:n.props.post}))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("This post was edited %(edits)s time.","This post was edited %(edits)s times.",this.props.post.edits);interpolate(a,{edits:this.props.post.edits},!0);return c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"edit"),gettext("Changes history")))}}]),t}(c["default"].Component),j=a.Approve=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.approve(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_approve&&this.props.post.is_unapproved?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}}]),t}(c["default"].Component),C=a.Move=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(g["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}}]),t}(c["default"].Component),S=a.Split=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(w["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_split"),gettext("Split"))):null}}]),t}(c["default"].Component),M=a.Protect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.protect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_protect?this.props.post.is_protected?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Protect"))):null}}]),t}(c["default"].Component),T=a.Unprotect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unprotect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_protect&&this.props.post.is_protected?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Remove protection"))):null}}]),t}(c["default"].Component),L=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.hide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id===a.best_answer?null:t.acl.can_hide?t.is_hidden?null:c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}}]),t}(c["default"].Component),A=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.unhide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return this.props.post.acl.can_unhide&&this.props.post.is_hidden?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}}]),t}(c["default"].Component),R=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){b.remove(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return t.id===a.best_answer?null:t.acl.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}}]),t}(c["default"].Component)},{"../../../../services/modal":370,"../../../../services/posting":374,"../../../post-changelog":118,"./actions":154,"./move":157,"./split":158,react:"react"}],156:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"pull-right dropdown"},o["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default btn-icon dropdown-toggle","data-toggle":"dropdown",type:"button"},o["default"].createElement("span",{className:"material-icon"},"expand_more")),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./dropdown"),s=n(l)},{"./dropdown":155,react:"react"}],157:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Move post")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../button"),p=(r(f),e("../../../form")),m=r(p),h=e("../../../form-group"),b=r(h),v=e("../../../../reducers/post"),g=n(v),_=e("../../../../services/ajax"),y=r(_),E=e("../../../../services/modal"),w=r(E),O=e("../../../../services/snackbar"),k=r(O),N=e("../../../../services/store"),x=r(N),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(k["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return y["default"].post(this.props.thread.api.posts.move,{new_thread:this.state.url,posts:[this.props.post.id]})}},{key:"handleSuccess",value:function(e){x["default"].dispatch(g.patch(this.props.post,{isDeleted:!0})),w["default"].hide(),k["default"].success(gettext("Selected post was moved to the other thread."))}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail):k["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(b["default"],{"for":"id_url",label:gettext("Link to thread you want to move post to")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading},gettext("Move post"))))))}}]),t}(m["default"]);a["default"]=P},{"../../../../reducers/post":352,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../button":8,"../../../form":55,"../../../form-group":54,react:"react"}],158:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement(k["default"],null))}function u(e){return m["default"].createElement(c,{className:"modal-dialog modal-message"},m["default"].createElement("div",{className:"message-icon"},m["default"].createElement("span",{className:"material-icon"},"info_outline")),m["default"].createElement("div",{className:"message-body"},m["default"].createElement("p",{className:"lead"},gettext("You can't move this post at the moment.")),m["default"].createElement("p",null,e.message)))}function c(e){return m["default"].createElement("div",{className:e.className,role:"document"},m["default"].createElement("div",{className:"modal-content"},m["default"].createElement("div",{className:"modal-header"},m["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},m["default"].createElement("span",{"aria-hidden":"true"},"×")),m["default"].createElement("h4",{className:"modal-title"},gettext("Split post into new thread"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0}),a.ModerationForm=a.PostingConfig=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return m["default"].createElement(B,f({},e,{Form:H}))},a.Loader=i,a.Error=u,a.Modal=c;var p=e("react"),m=r(p),h=e("../../../button"),b=r(h),v=e("../../../form"),g=r(v),_=e("../../../form-group"),y=r(_),E=e("../../../category-select"),w=r(E),O=e("../../../modal-loader"),k=r(O),N=e("../../../select"),x=r(N),P=e("../../../../reducers/post"),j=n(P),C=e("../../../../services/ajax"),S=r(C),M=e("../../../../services/modal"),T=r(M),L=e("../../../../services/snackbar"),A=r(L),R=e("../../../../services/store"),I=r(R),D=e("../../../../utils/validators"),U=n(D),B=a.PostingConfig=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isError:!1,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;S["default"].get(misago.get("THREAD_EDITOR_API")).then(function(t){var a=t.map(function(e){return Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id,post:e.post})});e.setState({isLoaded:!0,categories:a})},function(t){e.setState({isError:t.detail})})}},{key:"render",value:function(){return this.state.isError?m["default"].createElement(u,{message:this.state.isError}):this.state.isLoaded?m["default"].createElement(H,f({},this.props,{categories:this.state.categories})):m["default"].createElement(i,null)}}]),t}(m["default"].Component),H=a.ModerationForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,categories:e.categories,weight:0,is_hidden:0,is_closed:!1,validators:{title:[U.required()]},errors:{}},a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a.acl={},a.props.categories.forEach(function(e){e.post&&(a.state.category||(a.state.category=e.id),a.acl[e.id]={can_pin_threads:e.post.pin,can_close_threads:e.post.close,can_hide_threads:e.post.hide})}),a}return s(t,e),d(t,[{key:"clean",value:function(){return!!this.isValid()||(A["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return S["default"].post(this.props.thread.api.posts.split,{title:this.state.title,category:this.state.category,weight:this.state.weight,is_hidden:this.state.is_hidden,is_closed:this.state.is_closed,posts:[this.props.post.id]})}},{key:"handleSuccess",value:function(e){I["default"].dispatch(j.patch(this.props.post,{isDeleted:!0})),T["default"].hide(),A["default"].success(gettext("Selected post was split into new thread."))}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),A["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?T["default"].show(m["default"].createElement(ErrorsModal,{errors:e})):A["default"].apiError(e)}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?m["default"].createElement(y["default"],{label:gettext("Thread weight"),"for":"id_weight",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?m["default"].createElement(y["default"],{label:gettext("Hide thread"),"for":"id_is_hidden",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?m["default"].createElement(y["default"],{label:gettext("Close thread"),"for":"id_is_closed",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"render",value:function(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement("form",{onSubmit:this.handleSubmit},m["default"].createElement("div",{className:"modal-body"},m["default"].createElement(y["default"],{label:gettext("Thread title"),"for":"id_title",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.title},m["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),m["default"].createElement("div",{className:"clearfix"}),m["default"].createElement(y["default"],{label:gettext("Category"),"for":"id_category",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.category},m["default"].createElement(w["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.state.categories})),m["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),m["default"].createElement("div",{className:"modal-footer"},m["default"].createElement(b["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Split post")))))}}]),t}(g["default"])},{"../../../../reducers/post":352,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../../utils/validators":392,"../../../button":8,"../../../category-select":21,"../../../form":55,"../../../form-group":54,"../../../modal-loader":60,"../../../select":209,react:"react"}],159:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.post,a=e.thread,n=e.user;if(!i(t)||t.id!==a.best_answer)return null;var r=null;return r=n.id&&a.best_answer_marked_by===n.id?interpolate(gettext("Marked as best answer by you %(marked_on)s."),{marked_on:a.best_answer_marked_on.fromNow()},!0):interpolate(gettext("Marked as best answer by %(marked_by)s %(marked_on)s."),{marked_by:a.best_answer_marked_by_name,marked_on:a.best_answer_marked_on.fromNow()},!0),c["default"].createElement("div",{className:"post-status-message post-status-best-answer"},c["default"].createElement("span",{className:"material-icon"},"check_box"),c["default"].createElement("p",null,r))}function o(e){return i(e.post)&&e.post.is_hidden?c["default"].createElement("div",{className:"post-status-message post-status-hidden"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),c["default"].createElement("p",null,gettext("This post is hidden. Only users with permission may see its contents."))):null}function l(e){return i(e.post)&&e.post.is_unapproved?c["default"].createElement("div",{className:"post-status-message post-status-unapproved"},c["default"].createElement("span",{className:"material-icon"},"remove_circle_outline"),c["default"].createElement("p",null,gettext("This post is unapproved. Only users with permission to approve posts and its author may see its contents."))):null}function s(e){return i(e.post)&&e.post.is_protected?c["default"].createElement("div",{className:"post-status-message post-status-protected visible-xs-block"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),c["default"].createElement("p",null,gettext("This post is protected. Only moderators may change it."))):null}function i(e){return!e.is_hidden||e.acl.can_see_hidden}Object.defineProperty(a,"__esModule",{value:!0}),a.FlagBestAnswer=r,a.FlagHidden=o,a.FlagUnapproved=l,a.FlagProtected=s,a.isVisible=i;var u=e("react"),c=n(u)},{react:"react"}],160:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return(!e.is_hidden||e.acl.can_see_hidden)&&(e.acl.can_reply||e.acl.can_edit||e.acl.can_see_likes&&(e.last_likes||[]).length||e.acl.can_like)}function u(e,t){var a=t.slice(0,3).map(function(e){return e.username});if(1==a.length)return interpolate(gettext("%(user)s likes this."),{user:a[0]},!0);var n=e-a.length,r=a.slice(0,-1).join(", "),o=a.slice(-1)[0],l=interpolate(gettext("%(users)s and %(last_user)s"),{users:r,last_user:o},!0);if(0===n)return interpolate(gettext("%(users)s like this."),{users:l},!0);var s=ngettext("%(users)s and %(likes)s other user like this.","%(users)s and %(likes)s other users like this.",n);return interpolate(s,{users:a.join(", "),likes:n},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a.Edit=a.Reply=a.LikesCompact=a.Likes=a.Like=a.MarkAsBestAnswerCompact=a.MarkAsBestAnswer=void 0;var c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),d=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return i(e.post)?p["default"].createElement("div",{className:"post-footer"},p["default"].createElement(w,e),p["default"].createElement(O,e),p["default"].createElement(k,e),p["default"].createElement(N,d({lastLikes:e.post.last_likes,likes:e.post.likes},e)),p["default"].createElement(x,d({likes:e.post.likes},e)),p["default"].createElement(P,e),p["default"].createElement(j,e)):null},a.isVisible=i,a.getLikesMessage=u;var f=e("react"),p=r(f),m=e("./controls/actions"),h=n(m),b=e("../../post-likes"),v=r(b),g=e("../../../services/modal"),_=r(g),y=e("../../../services/posting"),E=r(y),w=a.MarkAsBestAnswer=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){h.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?a.best_answer&&!a.acl.can_change_best_answer?null:p["default"].createElement("button",{className:"hidden-xs btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||t.id===a.best_answer,onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Best answer")):null}}]),t}(p["default"].Component),O=a.MarkAsBestAnswerCompact=function(e){
+function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){h.markAsBestAnswer(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.post,a=e.thread;return a.acl.can_mark_best_answer&&t.acl.can_mark_as_best_answer?a.best_answer&&!a.acl.can_change_best_answer?null:p["default"].createElement("button",{className:"visible-xs-inline-block btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||t.id===a.best_answer,onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"check_box")):null}}]),t}(p["default"].Component),k=a.Like=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.post.is_liked?h.unlike(n.props):h.like(n.props)},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){if(!this.props.post.acl.can_like)return null;var e="btn btn-default btn-sm pull-left";return this.props.post.is_liked&&(e="btn btn-success btn-sm pull-left"),p["default"].createElement("button",{className:e,disabled:this.props.post.isBusy,onClick:this.onClick,type:"button"},this.props.post.is_liked?gettext("Liked"):gettext("Like"))}}]),t}(p["default"].Component),N=a.Likes=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){_["default"].show(p["default"].createElement(v["default"],{post:n.props.post}))},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?p["default"].createElement("button",{className:"btn btn-link btn-sm pull-left hidden-xs",onClick:this.onClick,type:"button"},u(this.props.likes,this.props.lastLikes)):p["default"].createElement("p",{className:"pull-left hidden-xs"},u(this.props.likes,this.props.lastLikes)):null}}]),t}(p["default"].Component),x=a.LikesCompact=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),c(t,[{key:"render",value:function(){var e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?p["default"].createElement("button",{className:"btn btn-link btn-sm likes-compact pull-left visible-xs-block",onClick:this.onClick,type:"button"},p["default"].createElement("span",{className:"material-icon"},"favorite"),this.props.likes):p["default"].createElement("p",{className:"likes-compact pull-left visible-xs-block"},p["default"].createElement("span",{className:"material-icon"},"favorite"),this.props.likes):null}}]),t}(N),P=a.Reply=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].open({mode:"REPLY",config:n.props.thread.api.editor,submit:n.props.thread.api.posts.index,context:{reply:n.props.post.id}})},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){return this.props.post.acl.can_reply?p["default"].createElement("button",{className:"btn btn-primary btn-sm pull-right",type:"button",onClick:this.onClick},gettext("Reply")):null}}]),t}(p["default"].Component),j=a.Edit=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){E["default"].open({mode:"EDIT",config:n.props.post.api.editor,submit:n.props.post.api.index})},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){return this.props.post.acl.can_edit?p["default"].createElement("button",{className:"hidden-xs btn btn-default btn-sm pull-right",type:"button",onClick:this.onClick},gettext("Edit")):null}}]),t}(p["default"].Component)},{"../../../services/modal":370,"../../../services/posting":374,"../../post-likes":131,"./controls/actions":154,react:"react"}],161:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.post.is_read?null:m["default"].createElement("span",{className:"label label-unread hidden-xs"},gettext("New post"))}function i(e){return e.post.is_read?null:m["default"].createElement("span",{className:"label label-unread visible-xs-inline-block"},gettext("New"))}function u(e){var t=interpolate(gettext("posted %(posted_on)s"),{posted_on:e.post.posted_on.format("LL, LT")},!0);return m["default"].createElement("a",{href:e.post.url.index,className:"btn btn-link posted-on hidden-xs",title:t},e.post.posted_on.fromNow())}function c(e){return m["default"].createElement("a",{href:e.post.url.index,className:"btn btn-link posted-on visible-xs-inline-block"},e.post.posted_on.fromNow(!0))}function d(e){var t=e.post.poster&&e.post.poster.id===e.user.id,a=e.post.acl.can_protect,n=e.user.id&&e.post.is_protected&&(t||a);return n?m["default"].createElement("span",{className:"label label-protected hidden-xs",title:gettext("This post is protected and may not be edited.")},m["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("protected")):null}Object.defineProperty(a,"__esModule",{value:!0}),a.PostEditsCompacts=a.PostEdits=void 0;var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return m["default"].createElement("div",{className:"post-heading"},m["default"].createElement(s,e),m["default"].createElement(i,e),m["default"].createElement(u,e),m["default"].createElement(c,e),m["default"].createElement(O,e),m["default"].createElement(k,e),m["default"].createElement(d,e),m["default"].createElement(g["default"],e),m["default"].createElement(b["default"],e))},a.UnreadLabel=s,a.UnreadCompact=i,a.PostedOn=u,a.PostedOnCompact=c,a.ProtectedLabel=d;var p=e("react"),m=n(p),h=e("./controls"),b=n(h),v=e("./select"),g=n(v),_=(e("../../user-status"),e("../../post-changelog")),y=n(_),E=e("../../../services/modal"),w=n(E),O=a.PostEdits=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){w["default"].show(m["default"].createElement(y["default"],{post:n.props.post}))},l=a,o(n,l)}return l(t,e),f(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("This post was edited %(edits)s time.","This post was edited %(edits)s times.",this.props.post.edits),n=interpolate(a,{edits:this.props.post.edits},!0),r=ngettext("edited %(edits)s time","edited %(edits)s times",this.props.post.edits);return m["default"].createElement("button",{className:"btn btn-link btn-see-edits hidden-xs",onClick:this.onClick,title:n,type:"button"},interpolate(r,{edits:this.props.post.edits},!0))}}]),t}(m["default"].Component),k=a.PostEditsCompacts=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),f(t,[{key:"render",value:function(){var e=this.props.post.is_hidden&&!this.props.post.acl.can_see_hidden,t=0===this.props.post.edits;if(e||t)return null;var a=ngettext("%(edits)s edit","%(edits)s edits",this.props.post.edits);return m["default"].createElement("button",{className:"btn btn-link btn-see-edits visible-xs-inline-block",onClick:this.onClick,type:"button"},interpolate(a,{edits:this.props.post.edits},!0))}}]),t}(O)},{"../../../services/modal":370,"../../post-changelog":118,"../../user-status":273,"./controls":156,"./select":171,react:"react"}],162:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t="post";return e.post.isDeleted?t="hide":e.post.is_hidden&&!e.post.acl.can_see_hidden&&(t="post post-hidden"),e.post.poster&&e.post.poster.rank.css_class&&(t+=" post-"+e.post.poster.rank.css_class),e.post.is_read||(t+=" post-new"),o["default"].createElement("li",{id:"post-"+e.post.id,className:t},o["default"].createElement("div",{className:"panel panel-default panel-post"},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"row"},o["default"].createElement(b["default"],e),o["default"].createElement("div",{className:"col-xs-12 col-md-9"},o["default"].createElement(m["default"],e),o["default"].createElement(c.FlagBestAnswer,e),o["default"].createElement(c.FlagUnapproved,e),o["default"].createElement(c.FlagProtected,e),o["default"].createElement(c.FlagHidden,e),o["default"].createElement(u["default"],e),o["default"].createElement(s["default"],e),o["default"].createElement(f["default"],e))))))};var r=e("react"),o=n(r),l=e("./attachments"),s=n(l),i=e("./body"),u=n(i),c=e("./flags"),d=e("./footer"),f=n(d),p=e("./header"),m=n(p),h=e("./post-side"),b=n(h)},{"./attachments":152,"./body":153,"./flags":159,"./footer":160,"./header":161,"./post-side":165,react:"react"}],163:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.thread;return o["default"].createElement("div",{className:"col-xs-12 col-md-3 post-side post-side-anonymous"},o["default"].createElement(d["default"],{post:t,thread:a}),o["default"].createElement(u["default"],{post:t,thread:a}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("span",null,o["default"].createElement(s["default"],{className:"poster-avatar",size:100}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("span",{className:"media-heading item-title"},t.poster_name),o["default"].createElement("span",{className:"user-title user-title-anonymous"},gettext("Removed user")))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("../controls"),u=n(i),c=e("../select"),d=n(c),f=e("../../../user-status"),p=(n(f),e("./user-postcount")),m=(n(p),e("./user-title"));n(m)},{"../../../avatar":6,"../../../user-status":273,"../controls":156,"../select":171,"./user-postcount":167,"./user-title":169,react:"react"}],164:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.title,a=e.rank;return a.is_tab||!!t||!!a.title}},{}],165:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.post.poster?o["default"].createElement(u["default"],e):o["default"].createElement(s["default"],e)};var r=e("react"),o=n(r),l=e("./anonymous"),s=n(l),i=e("./registered"),u=n(i)},{"./anonymous":163,"./registered":166,react:"react"}],166:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.post,a=e.thread,n=t.poster;return o["default"].createElement("div",{className:"col-xs-12 col-md-3 post-side post-side-registered"},o["default"].createElement(d["default"],{post:t,thread:a}),o["default"].createElement(u["default"],{post:t,thread:a}),o["default"].createElement("div",{className:"media"},o["default"].createElement("div",{className:"media-left"},o["default"].createElement("a",{href:n.url},o["default"].createElement(s["default"],{className:"poster-avatar",size:100,user:n}))),o["default"].createElement("div",{className:"media-body"},o["default"].createElement("div",{className:"media-heading"},o["default"].createElement("a",{className:"item-title",href:n.url},n.username),o["default"].createElement(p["default"],{status:n.status},o["default"].createElement(f.StatusIcon,{status:n.status}))),o["default"].createElement(_["default"],{rank:n.rank,title:n.title}),o["default"].createElement(v["default"],{poster:n}),o["default"].createElement(h["default"],{poster:n}))))};var r=e("react"),o=n(r),l=e("../../../avatar"),s=n(l),i=e("../controls"),u=n(i),c=e("../select"),d=n(c),f=e("../../../user-status"),p=n(f),m=e("./user-postcount"),h=n(m),b=e("./user-status"),v=n(b),g=e("./user-title"),_=n(g)},{"../../../avatar":6,"../../../user-status":273,"../controls":156,"../select":171,"./user-postcount":167,"./user-status":168,"./user-title":169,react:"react"}],167:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.poster,a=ngettext("%(posts)s post","%(posts)s posts",t.posts),n="user-postcount";return(0,s["default"])(t)&&(n+=" hidden-xs hidden-sm"),o["default"].createElement("span",{className:n},interpolate(a,{posts:t.posts},!0))};var r=e("react"),o=n(r),l=e("./has-visible-title"),s=n(l)},{"./has-visible-title":164,react:"react"}],168:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.poster,a="hidden-xs";return(0,u["default"])(t)&&(a+=" hidden-sm"),o["default"].createElement("span",{className:a},o["default"].createElement(s["default"],{status:t.status},o["default"].createElement(l.StatusLabel,{status:t.status,user:t})))};var r=e("react"),o=n(r),l=e("../../../user-status"),s=n(l),i=e("./has-visible-title"),u=n(i)},{"../../../user-status":273,"./has-visible-title":164,react:"react"}],169:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.rank,a=e.title,n=a||t.title;if(!n&&t.is_tab&&(n=t.name),!n)return null;var r="user-title";return t.css_class&&(r+=" user-title-"+t.css_class),t.is_tab?o["default"].createElement("div",{className:r},o["default"].createElement("a",{href:t.url},n)):o["default"].createElement("div",{className:r},n)};var r=e("react"),o=n(r)},{react:"react"}],170:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("li",{className:"post"},l["default"].createElement("div",{className:"post-border"},l["default"].createElement("div",{className:"post-avatar"},l["default"].createElement(i["default"],{size:"100"})),l["default"].createElement("div",{className:"post-body"},l["default"].createElement("div",{className:"panel panel-default panel-post"},l["default"].createElement("div",{className:"panel-heading post-heading"},l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,100)+"px"}}," "),l["default"].createElement("span",{className:"ui-preview-text",style:{width:c["int"](30,100)+"px"}}," ")),l["default"].createElement("div",{className:"panel-body"},l["default"].createElement("article",{className:"misago-markup"},l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," "),l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," "),l["default"].createElement("p",{className:"ui-preview-text",style:{width:c["int"](50,100)+"%"}}," ")))))))};var o=e("react"),l=r(o),s=e("../../avatar"),i=r(s),u=e("../../../utils/random"),c=n(u)},{"../../../utils/random":387,"../../avatar":6,react:"react"}],171:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.can_approve||e.can_hide||e.can_protect||e.can_unhide||e.can_delete||e.can_move}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.isVisible=i;var c=e("react"),d=r(c),f=e("../../../reducers/posts"),p=n(f),m=e("../../../services/store"),h=r(m),b=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.post.isSelected?h["default"].dispatch(p.deselect(n.props.post)):h["default"].dispatch(p.select(n.props.post))},r=a,l(n,r)}return s(t,e),u(t,[{key:"render",value:function(){return this.props.thread.acl.can_merge_posts||i(this.props.post.acl)?d["default"].createElement("div",{className:"pull-right hidden-xs"},d["default"].createElement("button",{className:"btn btn-default btn-icon",onClick:this.onClick,type:"button"},d["default"].createElement("span",{className:"material-icon"},this.props.post.isSelected?"check_box":"check_box_outline_blank"))):null}}]),t}(d["default"].Component);a["default"]=b},{"../../../reducers/posts":353,"../../../services/store":376,react:"react"}],172:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../reducers/post"),f=n(d),p=e("../../reducers/thread"),m=n(p),h=e("../../services/ajax"),b=r(h),v=e("../../services/snackbar"),g=r(v),_=e("../../services/store"),y=r(_),E=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;this.props.post.is_read||$(this.documentNode).waypoint({handler:function(t){"down"!==t||e.props.post.is_read||window.setTimeout(function(){var t=e.documentNode.getBoundingClientRect(),a=t.height+t.top,n=document.documentElement.clientHeight;a<5||a>n||(y["default"].dispatch(f.patch(e.props.post,{is_read:!0})),b["default"].post(e.props.post.api.read).then(function(t){y["default"].dispatch(m.update(e.props.thread,{is_read:t.thread_is_read}))},function(e){g["default"].apiError(e)}))},1e3)},offset:"bottom-in-view"})}},{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:this.props.className,ref:function(t){e.documentNode=t}},this.props.children)}}]),t}(c["default"].Component);a["default"]=E},{"../../reducers/post":352,"../../reducers/thread":359,"../../services/ajax":364,"../../services/snackbar":375,"../../services/store":376,react:"react"}],173:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("moment"),u=n(i),c=e("react"),d=n(c),f=e("../panel-loader"),p=n(f),m=e("../panel-message"),h=n(m),b=e("../../index"),v=n(b),g=e("../../services/polls"),_=n(g),y=e("../../services/page-title"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){e.expires_on&&(e.expires_on=(0,u["default"])(e.expires_on)),a.setState({isLoaded:!0,error:null,ban:e})},a.error=function(e){a.setState({isLoaded:!0,error:e.detail,ban:null})},v["default"].has("PROFILE_BAN")?a.initWithPreloadedData(v["default"].pop("PROFILE_BAN")):a.initWithoutPreloadedData(),a.startPolling(e.profile.api.ban),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){e.expires_on&&(e.expires_on=(0,u["default"])(e.expires_on)),this.state={isLoaded:!0,ban:e}}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(e){_["default"].start({poll:"ban-details",url:e,frequency:9e4,update:this.update,error:this.error})}},{key:"componentDidMount",value:function(){E["default"].set({title:gettext("Ban details"),parent:this.props.profile.username})}},{key:"componentWillUnmount",value:function(){_["default"].stop("ban-details")}},{key:"getUserMessage",value:function(){return this.state.ban.user_message?d["default"].createElement("div",{className:"panel-body ban-message ban-user-message"},d["default"].createElement("h4",null,gettext("User-shown ban message")),d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.user_message.html}})):null}},{key:"getStaffMessage",value:function(){return this.state.ban.staff_message?d["default"].createElement("div",{className:"panel-body ban-message ban-staff-message"},d["default"].createElement("h4",null,gettext("Team-shown ban message")),d["default"].createElement("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.staff_message.html}})):null}},{key:"getExpirationMessage",value:function(){if(this.state.ban.expires_on){if(this.state.ban.expires_on.isAfter((0,u["default"])())){var e=interpolate(gettext("This ban expires on %(expires_on)s."),{expires_on:this.state.ban.expires_on.format("LL, LT")},!0),t=interpolate(gettext("This ban expires %(expires_on)s."),{expires_on:this.state.ban.expires_on.fromNow()},!0);return d["default"].createElement("abbr",{title:e},t)}return gettext("This ban has expired.")}return interpolate(gettext("%(username)s's ban is permanent."),{username:this.props.profile.username},!0)}},{key:"getPanelBody",value:function(){return this.state.ban?Object.keys(this.state.ban).length?d["default"].createElement("div",null,this.getUserMessage(),this.getStaffMessage(),d["default"].createElement("div",{className:"panel-body ban-expires"},d["default"].createElement("h4",null,gettext("Ban expiration")),d["default"].createElement("p",{className:"lead"},this.getExpirationMessage()))):d["default"].createElement("div",null,d["default"].createElement(h["default"],{message:gettext("No ban is active at the moment.")})):this.state.error?d["default"].createElement("div",null,d["default"].createElement(h["default"],{icon:"error_outline",message:this.state.error})):d["default"].createElement("div",null,d["default"].createElement(p["default"],null))}},{key:"render",value:function(){return d["default"].createElement("div",{className:"profile-ban-details"},d["default"].createElement("div",{className:"panel panel-default"},d["default"].createElement("div",{className:"panel-heading"},d["default"].createElement("h3",{className:"panel-title"},gettext("Ban details"))),this.getPanelBody()))}}]),t}(d["default"].Component);a["default"]=w},{"../../index":301,"../../services/page-title":372,"../../services/polls":373,"../panel-loader":92,"../panel-message":93,moment:"moment",react:"react"}],174:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.isAuthenticated,a=e.profile,n=null;return n=t?gettext("You are not sharing any details with others."):interpolate(gettext("%(username)s is not sharing any details with others."),{username:a.username},!0),o["default"].createElement("div",{className:"panel panel-default"},o["default"].createElement("div",{className:"panel-body text-center lead"},n))};var r=e("react"),o=n(r)},{react:"react"}],175:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.text,a=e.url;return a?l["default"].createElement("p",null,l["default"].createElement("a",{href:a,target:"_blank",rel:"nofollow"},t||a)):t?l["default"].createElement("p",null,t):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.html,a=e.text,n=e.url;return t?l["default"].createElement("div",{className:"form-control-static col-md-9",dangerouslySetInnerHTML:{__html:t}}):l["default"].createElement("div",{className:"form-control-static col-md-9"},l["default"].createElement(r,{text:a,url:n}))},a.SafeValue=r;var o=e("react"),l=n(o)},{react:"react"}],176:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"form-group"},o["default"].createElement("strong",{className:"control-label col-md-3"},e.name,":"),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./field-value"),s=n(l)},{"./field-value":175,react:"react"}],177:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.api,a=e.display,n=e.onCancel,r=e.onSuccess;return a?o["default"].createElement(s["default"],{api:t,onCancel:n,onSuccess:r}):null};var r=e("react"),o=n(r),l=e("../../edit-details"),s=n(l)},{"../../edit-details":32,react:"react"}],178:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.fields,a=e.name;return o["default"].createElement("div",{className:"panel panel-default panel-profile-details-group"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},a)),o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"form-horizontal"},t.map(function(e){var t=e.fieldname,a=e.html,n=e.name,r=e.text,l=e.url;return o["default"].createElement(s["default"],{key:t,name:n,html:a,text:r,url:l})}))))};var r=e("react"),o=n(r),l=e("./field"),s=n(l)},{"./field":176,react:"react"}],179:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.display,a=e.groups,n=e.isAuthenticated,r=e.loading,l=e.profile;return t?r?o["default"].createElement(d["default"],null):a.length?o["default"].createElement("div",null,a.map(function(e,t){return o["default"].createElement(u["default"],{fields:e.fields,key:t,name:e.name})})):o["default"].createElement(s["default"],{isAuthenticated:n,profile:l}):null};var r=e("react"),o=n(r),l=e("./empty-message"),s=n(l),i=e("./group"),u=n(i),c=e("../../loader"),d=n(c)},{"../../loader":57,"./empty-message":174,"./group":178,react:"react"}],180:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.onEdit,a=e.showEditButton;return a?l["default"].createElement("div",{className:"col-sm-4 col-md-2"},l["default"].createElement("button",{className:"btn btn-default btn-outline btn-block",onClick:t,type:"button"},gettext("Edit"))):null}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.onEdit,a=e.showEditButton;return l["default"].createElement("div",null,l["default"].createElement("nav",{className:"toolbar"},l["default"].createElement("div",{className:"row"},l["default"].createElement("div",{className:"col-sm-8 col-md-10"},l["default"].createElement("h3",{className:"md-margin-top-no"},gettext("Details"))),l["default"].createElement(r,{onEdit:t,showEditButton:a}))))},a.EditButton=r;var o=e("react"),l=n(o)},{react:"react"}],181:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./form"),d=n(c),f=e("./groups-list"),p=n(f),m=e("./header"),h=n(m),b=e("../../../data/profile-details"),v=n(b),g=e("../../../reducers/profile-details"),_=e("../../../services/page-title"),y=n(_),E=e("../../../services/snackbar"),w=n(E),O=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCancel=function(){a.setState({editing:!1})},a.onEdit=function(){a.setState({editing:!0})},a.onSuccess=function(e){
+var t=a.props,n=t.dispatch,r=t.isAuthenticated,o=t.profile,l=null;l=r?gettext("Your details have been updated."):interpolate(gettext("%(username)s's details have been updated."),{username:o.username},!0),w["default"].info(l),n((0,g.load)(e)),a.setState({editing:!1})},a.state={editing:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){y["default"].set({title:gettext("Details"),parent:this.props.profile.username})}},{key:"render",value:function(){var e=this.props,t=e.dispatch,a=e.isAuthenticated,n=e.profile,r=e.profileDetails,o=r.id!==n.id;return u["default"].createElement(v["default"],{data:r,dispatch:t,user:n},u["default"].createElement("div",{className:"profile-details"},u["default"].createElement(h["default"],{onEdit:this.onEdit,showEditButton:!!r.edit&&!this.state.editing}),u["default"].createElement(p["default"],{display:!this.state.editing,groups:r.groups,isAuthenticated:a,loading:o,profile:n}),u["default"].createElement(d["default"],{api:n.api.edit_details,dispatch:t,display:this.state.editing,onCancel:this.onCancel,onSuccess:this.onSuccess})))}}]),t}(u["default"].Component);a["default"]=O},{"../../../data/profile-details":300,"../../../reducers/profile-details":354,"../../../services/page-title":372,"../../../services/snackbar":375,"./form":177,"./groups-list":179,"./header":180,react:"react"}],182:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=null;t=e.user.id===e.profile.id?gettext("You have no started threads."):interpolate(gettext("%(username)s started no threads."),{username:e.profile.username},!0);var a=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){var n=ngettext("You have started %(threads)s thread.","You have started %(threads)s threads.",e.posts.count);a=interpolate(n,{threads:e.posts.count},!0)}else{var r=ngettext("%(username)s has started %(threads)s thread.","%(username)s has started %(threads)s threads.",e.posts.count);a=interpolate(r,{username:e.profile.username,threads:e.posts.count},!0)}else a=gettext("Loading...");return i["default"].createElement(c["default"],l({api:e.profile.api.threads,emptyMessage:t,header:a,title:gettext("Threads")},e))}function o(e){var t=null;t=e.user.id===e.profile.id?gettext("You have posted no messages."):interpolate(gettext("%(username)s posted no messages."),{username:e.profile.username},!0);var a=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){var n=ngettext("You have posted %(posts)s message.","You have posted %(posts)s messages.",e.posts.count);a=interpolate(n,{posts:e.posts.count},!0)}else{var r=ngettext("%(username)s has posted %(posts)s message.","%(username)s has posted %(posts)s messages.",e.posts.count);a=interpolate(r,{username:e.profile.username,posts:e.posts.count},!0)}else a=gettext("Loading...");return i["default"].createElement(c["default"],l({api:e.profile.api.posts,emptyMessage:t,header:a,title:gettext("Posts")},e))}Object.defineProperty(a,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a.Threads=r,a.Posts=o;var s=e("react"),i=n(s),u=e("./route"),c=n(u)},{"./route":183,react:"react"}],183:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e.posts.count?p["default"].createElement("div",null,p["default"].createElement(h["default"],{isReady:e.posts.isLoaded,posts:e.posts.results,poster:e.profile}),p["default"].createElement(u,{isLoading:e.isLoading,loadMore:e.loadMore,more:e.posts.more})):p["default"].createElement("p",{className:"lead"},e.emptyMessage)}function u(e){return e.more?p["default"].createElement("div",{className:"pager-more"},p["default"].createElement(v["default"],{className:"btn btn-default btn-outline",loading:e.isLoading,onClick:e.loadMore},interpolate(gettext("Show more (%(more)s)"),{more:e.more},!0))):null}Object.defineProperty(a,"__esModule",{value:!0});var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Feed=i,a.LoadMoreButton=u;var f=e("react"),p=r(f),m=e("../../post-feed"),h=r(m),b=e("../../button"),v=r(b),g=e("../../../reducers/posts"),_=n(g),y=e("../../../services/page-title"),E=r(y),w=e("../../../services/ajax"),O=r(w),k=e("../../../services/snackbar"),N=r(k),x=e("../../../services/store"),P=r(x),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isLoading:!0}),a.loadItems(a.props.posts.page+1)},a.state={isLoading:!1},a}return s(t,e),d(t,[{key:"loadItems",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;O["default"].get(this.props.api,{page:t||1}).then(function(a){1===t?P["default"].dispatch(_.load(a)):P["default"].dispatch(_.append(a)),e.setState({isLoading:!1})},function(t){e.setState({isLoading:!1}),N["default"].apiError(t)})}},{key:"componentDidMount",value:function(){E["default"].set({title:this.props.title,parent:this.props.profile.username}),this.loadItems()}},{key:"render",value:function(){return p["default"].createElement("div",{className:"profile-feed"},p["default"].createElement("nav",{className:"toolbar"},p["default"].createElement("h3",{className:"toolbar-left"},this.props.header)),p["default"].createElement(i,c({isLoading:this.state.isLoading,loadMore:this.loadMore},this.props)))}}]),t}(p["default"].Component);a["default"]=j},{"../../../reducers/posts":353,"../../../services/ajax":364,"../../../services/page-title":372,"../../../services/snackbar":375,"../../../services/store":376,"../../button":8,"../../post-feed":121,react:"react"}],184:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../../reducers/profile"),p=e("../../services/ajax"),m=n(p),h=e("../../services/snackbar"),b=n(h),v=e("../../services/store"),g=n(v),_=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.action=function(){a.setState({isLoading:!0}),a.props.profile.is_followed?g["default"].dispatch((0,f.patch)({is_followed:!1,followers:a.props.profile.followers-1})):g["default"].dispatch((0,f.patch)({is_followed:!0,followers:a.props.profile.followers+1})),m["default"].post(a.props.profile.api.follow).then(function(e){a.setState({isLoading:!1}),g["default"].dispatch((0,f.patch)(e))},function(e){a.setState({isLoading:!1}),b["default"].apiError(e)})},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.profile.is_followed?this.props.className+" btn-default btn-following":this.props.className+" btn-default btn-follow"}},{key:"getIcon",value:function(){return this.props.profile.is_followed?"favorite":"favorite_border"}},{key:"getLabel",value:function(){return this.props.profile.is_followed?gettext("Following"):gettext("Follow")}},{key:"render",value:function(){return u["default"].createElement(d["default"],{className:this.getClassName(),disabled:this.state.isLoading,onClick:this.action},u["default"].createElement("span",{className:"material-icon"},this.getIcon()),this.getLabel())}}]),t}(u["default"].Component);a["default"]=_},{"../../reducers/profile":355,"../../services/ajax":364,"../../services/snackbar":375,"../../services/store":376,"../button":8,react:"react"}],185:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../quick-search"),p=n(f),m=e("../users-list"),h=n(m),b=e("../../index"),v=n(b),g=e("../../reducers/users"),_=e("../../services/ajax"),y=n(_),E=e("../../services/snackbar"),w=n(E),O=e("../../services/store"),k=n(O),N=e("../../services/page-title"),x=n(N),P=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isBusy:!0}),a.loadUsers(a.state.page+1,a.state.search)},a.search=function(e){a.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),a.loadUsers(1,e.target.value)},a.setSpecialProps(),v["default"].has(a.PRELOADED_DATA_KEY)?a.initWithPreloadedData(v["default"].pop(a.PRELOADED_DATA_KEY)):a.initWithoutPreloadedData(),a}return l(t,e),s(t,[{key:"setSpecialProps",value:function(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWERS",this.TITLE=gettext("Followers"),this.API_FILTER="followers"}},{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},k["default"].dispatch((0,g.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadUsers()}},{key:"loadUsers",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=this.props.profile.api[this.API_FILTER];y["default"].get(n,{search:a,page:t||1},"user-"+this.API_FILTER).then(function(a){1===t?k["default"].dispatch((0,g.hydrate)(a.results)):k["default"].dispatch((0,g.append)(a.results)),e.setState({isLoaded:!0,isBusy:!1,count:a.count,more:a.more,page:a.page,pages:a.pages})},function(e){w["default"].apiError(e)})}},{key:"componentDidMount",value:function(){x["default"].set({title:this.TITLE,parent:this.props.profile.username})}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("You have %(users)s follower.","You have %(users)s followers.",this.state.count);return interpolate(t,{users:this.state.count},!0)}var a=ngettext("%(username)s has %(users)s follower.","%(username)s has %(users)s followers.",this.state.count);return interpolate(a,{username:this.props.profile.username,users:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("You have no followers."):interpolate(gettext("%(username)s has no followers."),{username:this.props.profile.username},!0)}},{key:"getMoreButton",value:function(){return this.state.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(d["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},interpolate(gettext("Show more (%(more)s)"),{more:this.state.more},!0))):null}},{key:"getListBody",value:function(){return this.state.isLoaded&&0===this.state.count?u["default"].createElement("p",{className:"lead"},this.getEmptyMessage()):u["default"].createElement("div",null,u["default"].createElement(h["default"],{cols:3,isReady:this.state.isLoaded,users:this.props.users}),this.getMoreButton())}},{key:"getClassName",value:function(){return"profile-"+this.API_FILTER}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("nav",{className:"toolbar"},u["default"].createElement("h3",{className:"toolbar-left"},this.getLabel()),u["default"].createElement(p["default"],{className:"toolbar-right",value:this.state.search,onChange:this.search,placeholder:gettext("Search users...")})),this.getListBody())}}]),t}(u["default"].Component);a["default"]=P},{"../../index":301,"../../reducers/users":363,"../../services/ajax":364,"../../services/page-title":372,"../../services/snackbar":375,"../../services/store":376,"../button":8,"../quick-search":196,"../users-list":283,react:"react"}],186:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=(n(i),e("./followers")),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"setSpecialProps",value:function(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWS",this.TITLE=gettext("Follows"),this.API_FILTER="follows"}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("You are following %(users)s user.","You are following %(users)s users.",this.state.count);return interpolate(t,{users:this.state.count},!0)}var a=ngettext("%(username)s is following %(users)s user.","%(username)s is following %(users)s users.",this.state.count);return interpolate(a,{username:this.props.profile.username,users:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("You are not following any users."):interpolate(gettext("%(username)s is not following any users."),{username:this.props.profile.username},!0)}}]),t}(c["default"]);a["default"]=d},{"./followers":185,react:"react"}],187:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.isActive,a=e.isDeletingAccount;if(t!==!1&&a!==!0)return null;var n=null;return n=a?gettext("This user is deleting their account."):gettext("This user's account has been disabled by administrator."),d["default"].createElement("div",{className:"alert alert-danger"},d["default"].createElement("p",null,n))}function i(e,t){var a="";return 1==e&&(a="col-xs-12"),2==e&&(a="col-xs-6 col-sm-6"),3==e&&(2==t?a="col-xs-12 col-sm-4 xs-margin-top":a+="col-xs-6 col-sm-4"),a}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.IsDisabledMessage=s,a.getColStyle=i;var c=e("react"),d=n(c),f=e("../avatar"),p=n(f),m=e("../dropdown-toggle"),h=(n(m),e("./follow-button")),b=n(h),v=e("./message-button"),g=n(v),_=e("./moderation/nav"),y=n(_),E=e("./navs"),w=e("../user-status"),O=n(w),k=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getUserStatus",value:function(){return d["default"].createElement("li",{className:"user-status-display"},d["default"].createElement(O["default"],{user:this.props.profile,status:this.props.profile.status},d["default"].createElement(w.StatusIcon,{user:this.props.profile,status:this.props.profile.status}),d["default"].createElement(w.StatusLabel,{user:this.props.profile,status:this.props.profile.status,className:"status-label"})))}},{key:"getUserRank",value:function(){return this.props.profile.rank.is_tab?d["default"].createElement("li",{className:"user-rank"},d["default"].createElement("a",{href:this.props.profile.rank.url,className:"item-title"},this.props.profile.rank.name)):d["default"].createElement("li",{className:"user-rank"},d["default"].createElement("span",{className:"item-title"},this.props.profile.rank.name))}},{key:"getUserTitle",value:function(){return this.props.profile.title?d["default"].createElement("li",{className:"user-title"},this.props.profile.title):this.props.profile.rank.title?d["default"].createElement("li",{className:"user-title"},this.props.profile.rank.title):null}},{key:"getJoinedOn",value:function(){var e=interpolate(gettext("Joined on %(joined_on)s"),{joined_on:this.props.profile.joined_on.format("LL, LT")},!0),t=interpolate(gettext("Joined %(joined_on)s"),{joined_on:this.props.profile.joined_on.fromNow()},!0);return d["default"].createElement("li",{className:"user-joined-on"},d["default"].createElement("abbr",{title:e},t))}},{key:"getEmail",value:function(){return this.props.profile.email?d["default"].createElement("li",{className:"user-email"},d["default"].createElement("a",{href:"mailto:"+this.props.profile.email,className:"item-title"},this.props.profile.email)):null}},{key:"getFollowButton",value:function(){return this.props.profile.acl.can_follow?d["default"].createElement(b["default"],{className:"btn btn-block btn-outline",profile:this.props.profile}):null}},{key:"getModerationButton",value:function(){return this.props.profile.acl.can_moderate?d["default"].createElement("div",{className:"btn-group btn-group-justified"},d["default"].createElement("div",{className:"btn-group"},d["default"].createElement("button",{className:"btn btn-default btn-moderate btn-outline dropdown-toggle",type:"button","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},d["default"].createElement("span",{className:"material-icon"},"tonality"),gettext("Moderation")),d["default"].createElement(y["default"],{profile:this.props.profile}))):null}},{key:"render",value:function(){var e=this.props.profile.acl.can_follow,t=this.props.profile.acl.can_moderate,a=this.props.user.id===this.props.profile.id,n=!a&&this.props.user.acl.can_start_private_threads,r=0;e&&(r+=1),t&&(r+=1),n&&(r+=1);var o=r?2*r+1:0,l="page-header";return this.props.profile.rank.css_class&&(l+=" page-header-rank-"+this.props.profile.rank.css_class),d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:l},d["default"].createElement("div",{className:"container"},d["default"].createElement(s,{isActive:this.props.profile.is_active,isDeletingAccount:this.props.profile.is_deleting_account}),d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-9 col-md-offset-3"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-"+(12-o)},d["default"].createElement(p["default"],{className:"user-avatar user-avatar-sm",user:this.props.profile,size:"100",size2x:"200"}),d["default"].createElement("h1",null,this.props.profile.username)),!!r&&d["default"].createElement("div",{className:"col-sm-"+o},d["default"].createElement("div",{className:"row xs-margin-top sm-margin-top"},!!n&&d["default"].createElement("div",{className:i(r,0)},d["default"].createElement(g["default"],{className:"btn btn-default btn-block btn-outline",profile:this.props.profile,user:this.props.user})),!!e&&d["default"].createElement("div",{className:i(r,1)},this.getFollowButton()),!!t&&d["default"].createElement("div",{className:i(r,2)},this.getModerationButton()))))))),d["default"].createElement("div",{className:"header-stats"},d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-9 col-md-offset-3"},d["default"].createElement("ul",{className:"list-inline"},this.getUserStatus(),this.getUserRank(),this.getUserTitle(),this.getJoinedOn(),this.getEmail()))))),d["default"].createElement(E.CompactNav,{baseUrl:this.props.baseUrl,pages:this.props.pages,profile:this.props.profile})))}}]),t}(d["default"].Component);a["default"]=k},{"../avatar":6,"../dropdown-toggle":27,"../user-status":273,"./follow-button":184,"./message-button":188,"./moderation/nav":192,"./navs":193,react:"react"}],188:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../services/posting"),d=n(c),f=e("../.."),p=n(f),m=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){d["default"].open({mode:"START_PRIVATE",submit:p["default"].get("PRIVATE_THREADS_API"),to:[n.props.profile]})},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.user.acl.can_start_private_threads,t=this.props.user.id===this.props.profile.id;return!e||t?null:u["default"].createElement("button",{className:this.props.className,onClick:this.onClick,type:"button"},u["default"].createElement("span",{className:"material-icon"},"comment"),gettext("Message"))}}]),t}(u["default"].Component);a["default"]=m},{"../..":301,"../../services/posting":374,react:"react"}],189:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../modal-loader"),v=n(b),g=e("../../yes-no-switch"),_=n(g),y=e("../../modal-message"),E=n(y),w=e("../../../reducers/users"),O=e("../../../services/ajax"),k=n(O),N=e("../../../services/snackbar"),x=n(N),P=e("../../../services/store"),j=n(P),C=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isLoading:!1,error:null,is_avatar_locked:"",avatar_lock_user_message:"",avatar_lock_staff_message:""},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;k["default"].get(this.props.profile.api.moderate_avatar).then(function(t){e.setState({isLoaded:!0,is_avatar_locked:t.is_avatar_locked,avatar_lock_user_message:t.avatar_lock_user_message||"",avatar_lock_staff_message:t.avatar_lock_staff_message||""})},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"clean",value:function(){return!!this.isValid()||(x["default"].error(this.validate().username[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.profile.api.moderate_avatar,{is_avatar_locked:this.state.is_avatar_locked,avatar_lock_user_message:this.state.avatar_lock_user_message,avatar_lock_staff_message:this.state.avatar_lock_staff_message})}},{key:"handleSuccess",value:function(e){j["default"].dispatch((0,w.updateAvatar)(this.props.profile,e.avatar_hash)),x["default"].success(gettext("Avatar controls have been changed."))}},{key:"getFormBody",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(h["default"],{label:gettext("Lock avatar"),helpText:gettext("Locking user avatar will prohibit user from changing his avatar and will reset his/her avatar to default one."),"for":"id_is_avatar_locked"},u["default"].createElement(_["default"],{id:"id_is_avatar_locked",disabled:this.state.isLoading,iconOn:"lock_outline",iconOff:"lock_open",labelOn:gettext("Disallow user from changing avatar"),labelOff:gettext("Allow user to change avatar"),onChange:this.bindInput("is_avatar_locked"),value:this.state.is_avatar_locked})),u["default"].createElement(h["default"],{label:gettext("User message"),helpText:gettext("Optional message for user explaining why he/she is prohibited form changing avatar."),"for":"id_avatar_lock_user_message"},u["default"].createElement("textarea",{id:"id_avatar_lock_user_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_user_message"),value:this.state.avatar_lock_user_message})),u["default"].createElement(h["default"],{label:gettext("Staff message"),helpText:gettext("Optional message for forum team members explaining why user is prohibited form changing avatar."),"for":"id_avatar_lock_staff_message"},u["default"].createElement("textarea",{id:"id_avatar_lock_staff_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_staff_message"),value:this.state.avatar_lock_staff_message}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},gettext("Close")),u["default"].createElement(d["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Save changes"))))}},{key:"getModalBody",value:function(){return this.state.error?u["default"].createElement(E["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():u["default"].createElement(v["default"],null)}},{key:"getClassName",value:function(){return this.state.error?"modal-dialog modal-message modal-avatar-controls":"modal-dialog modal-avatar-controls"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName(),role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Avatar controls"))),this.getModalBody()))}}]),t}(p["default"]);a["default"]=C},{"../../../reducers/users":363,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,"../../button":8,"../../form":55,"../../form-group":54,"../../modal-loader":60,"../../modal-message":61,"../../yes-no-switch":299,react:"react"}],190:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){
+var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../modal-loader"),g=r(v),_=e("../../modal-message"),y=r(_),E=e("../../../reducers/username-history"),w=e("../../../reducers/users"),O=e("../../../services/ajax"),k=r(O),N=e("../../../services/snackbar"),x=r(N),P=e("../../../services/store"),j=r(P),C=e("../../../utils/validators"),S=n(C),M=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isLoading:!1,error:null,username:"",validators:{username:[S.usernameContent()]}},a}return s(t,e),i(t,[{key:"componentDidMount",value:function(){var e=this;k["default"].get(this.props.profile.api.moderate_username).then(function(){e.setState({isLoaded:!0})},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"clean",value:function(){return!!this.isValid()||(x["default"].error(this.validate().username[0]),!1)}},{key:"send",value:function(){return k["default"].post(this.props.profile.api.moderate_username,{username:this.state.username})}},{key:"handleSuccess",value:function(e){this.setState({username:""}),j["default"].dispatch((0,E.addNameChange)(e,this.props.profile,this.props.user)),j["default"].dispatch((0,w.updateUsername)(this.props.profile,e.username,e.slug)),x["default"].success(gettext("Username has been changed."))}},{key:"getFormBody",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(b["default"],{label:gettext("New username"),"for":"id_username"},c["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Change username"))))}},{key:"getModalBody",value:function(){return this.state.error?c["default"].createElement(y["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():c["default"].createElement(g["default"],null)}},{key:"getClassName",value:function(){return this.state.error?"modal-dialog modal-message modal-rename-user":"modal-dialog modal-rename-user"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Change username"))),this.getModalBody()))}}]),t}(m["default"]);a["default"]=M},{"../../../reducers/username-history":362,"../../../reducers/users":363,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,"../../../utils/validators":392,"../../button":8,"../../form":55,"../../form-group":54,"../../modal-loader":60,"../../modal-message":61,react:"react"}],191:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../button"),d=n(c),f=e("../../form"),p=n(f),m=e("../../form-group"),h=n(m),b=e("../../modal-loader"),v=n(b),g=e("../../modal-message"),_=n(g),y=e("../../yes-no-switch"),E=n(y),w=e("../../../index"),O=n(w),k=e("../../../services/ajax"),N=n(k),x=e("../../../services/polls"),P=n(x),j=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.countdown=function(){window.setTimeout(function(){a.state.countdown>1?(a.setState({countdown:a.state.countdown-1}),a.countdown()):a.state.confirm||a.setState({confirm:!0})},1e3)},a.state={isLoaded:!1,isLoading:!1,isDeleted:!1,error:null,countdown:5,confirm:!1,with_content:!1},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;N["default"].get(this.props.profile.api["delete"]).then(function(){e.setState({isLoaded:!0}),e.countdown()},function(t){e.setState({isLoaded:!0,error:t.detail})})}},{key:"send",value:function(){return N["default"].post(this.props.profile.api["delete"],{with_content:this.state.with_content})}},{key:"handleSuccess",value:function(){P["default"].stop("user-profile"),this.state.with_content?this.setState({isDeleted:interpolate(gettext("%(username)s's account, threads, posts and other content has been deleted."),{username:this.props.profile.username},!0)}):this.setState({isDeleted:interpolate(gettext("%(username)s's account has been deleted and other content has been hidden."),{username:this.props.profile.username},!0)})}},{key:"getButtonLabel",value:function(){return this.state.confirm?interpolate(gettext("Delete %(username)s"),{username:this.props.profile.username},!0):interpolate(gettext("Please wait... (%(countdown)ss)"),{countdown:this.state.countdown},!0)}},{key:"getForm",value:function(){return u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(h["default"],{label:gettext("User content"),"for":"id_with_content"},u["default"].createElement(E["default"],{id:"id_with_content",disabled:this.state.isLoading,labelOn:gettext("Delete together with user's account"),labelOff:gettext("Hide after deleting user's account"),onChange:this.bindInput("with_content"),value:this.state.with_content}))),u["default"].createElement("div",{className:"modal-footer"},u["default"].createElement("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},gettext("Cancel")),u["default"].createElement(d["default"],{className:"btn-danger",loading:this.state.isLoading,disabled:!this.state.confirm},this.getButtonLabel())))}},{key:"getDeletedBody",value:function(){return u["default"].createElement("div",{className:"modal-body"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},"info_outline")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.state.isDeleted),u["default"].createElement("p",null,u["default"].createElement("a",{href:O["default"].get("USERS_LIST_URL")},gettext("Return to users list")))))}},{key:"getModalBody",value:function(){return this.state.error?u["default"].createElement(_["default"],{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.state.isDeleted?this.getDeletedBody():this.getForm():u["default"].createElement(v["default"],null)}},{key:"getClassName",value:function(){return this.state.error||this.state.isDeleted?"modal-dialog modal-message modal-delete-account":"modal-dialog modal-delete-account"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName(),role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Delete user account"))),this.getModalBody()))}}]),t}(p["default"]);a["default"]=j},{"../../../index":301,"../../../services/ajax":364,"../../../services/polls":373,"../../button":8,"../../form":55,"../../form-group":54,"../../modal-loader":60,"../../modal-message":61,"../../yes-no-switch":299,react:"react"}],192:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-redux"),d=e("./avatar-controls"),f=n(d),p=e("./change-username"),m=n(p),h=e("./delete-account"),b=n(h),v=e("../../../services/modal"),g=n(v),_=function(e){return{tick:e.tick,user:e.auth,profile:e.profile}},y=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showAvatarDialog=function(){g["default"].show((0,c.connect)(_)(f["default"]))},n.showRenameDialog=function(){g["default"].show((0,c.connect)(_)(m["default"]))},n.showDeleteDialog=function(){g["default"].show((0,c.connect)(_)(b["default"]))},l=a,o(n,l)}return l(t,e),s(t,[{key:"getAvatarButton",value:function(){return this.props.profile.acl.can_moderate_avatar?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showAvatarDialog},u["default"].createElement("span",{className:"material-icon"},"portrait"),gettext("Avatar controls"))):null}},{key:"getRenameButton",value:function(){return this.props.profile.acl.can_rename?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showRenameDialog},u["default"].createElement("span",{className:"material-icon"},"credit_card"),gettext("Change username"))):null}},{key:"getDeleteButton",value:function(){return this.props.profile.acl.can_delete?u["default"].createElement("li",null,u["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.showDeleteDialog},u["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete account"))):null}},{key:"render",value:function(){return u["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom",role:"menu"},this.getAvatarButton(),this.getRenameButton(),this.getDeleteButton())}}]),t}(u["default"].Component);a["default"]=y},{"../../../services/modal":370,"./avatar-controls":189,"./change-username":190,"./delete-account":191,react:"react","react-redux":"react-redux"}],193:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return c["default"].createElement("div",{className:"page-tabs hidden-md hidden-lg"},c["default"].createElement("div",{className:"container"},c["default"].createElement("ul",{className:"nav nav-pills",role:"menu"},e.pages.map(function(t){return c["default"].createElement(p["default"],{path:e.baseUrl+t.component+"/",key:t.component},c["default"].createElement(d.Link,{to:e.baseUrl+t.component+"/",onClick:e.hideNav},c["default"].createElement("span",{className:"material-icon"},t.icon),t.name))}))))}Object.defineProperty(a,"__esModule",{value:!0}),a.SideNav=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.CompactNav=s;var u=e("react"),c=n(u),d=e("react-router"),f=e("../li"),p=n(f),m=e("./follow-button"),h=(n(m),e("../../index"));n(h),a.SideNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"list-group nav-side"},this.props.pages.map(function(t){return c["default"].createElement(d.Link,{to:e.props.baseUrl+t.component+"/",className:"list-group-item",activeClassName:"active",key:t.component},c["default"].createElement("span",{className:"material-icon"},t.icon),t.name)}))}}]),t}(c["default"].Component)},{"../../index":301,"../li":56,"./follow-button":184,react:"react","react-router":"react-router"}],194:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{isAuthenticated:e.auth.user.id===e.profile.id,tick:e.tick.tick,user:e.auth.user,users:e.users,posts:e.posts,profile:e.profile,profileDetails:e["profile-details"],"username-history":e["username-history"]}}function i(){var e=[];return L["default"].get("PROFILE_PAGES").forEach(function(t){e.push(Object.assign({},t,{path:L["default"].get("PROFILE").url+t.component+"/",component:(0,f.connect)(s)(H[t.component])}))}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("./ban-details"),m=n(p),h=e("./details"),b=n(h),v=e("./feed"),g=e("./followers"),_=n(g),y=e("./follows"),E=n(y),w=e("./username-history"),O=n(w),k=e("./header"),N=n(k),x=e("./moderation/nav"),P=(n(x),e("./navs")),j=e("../avatar"),C=n(j),S=e("../with-dropdown"),M=n(S),T=e("../.."),L=n(T),A=e("../../reducers/profile"),R=e("../../services/polls"),I=n(R),D=e("../../services/store"),U=n(D),B=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){U["default"].dispatch((0,A.hydrate)(e))},a.startPolling(e.profile.api.index),a}return l(t,e),u(t,[{key:"startPolling",value:function(e){I["default"].start({poll:"user-profile",url:e,frequency:9e4,update:this.update})}},{key:"render",value:function(){var e=L["default"].get("PROFILE").url,t=L["default"].get("PROFILE_PAGES");return d["default"].createElement("div",{className:"page page-user-profile"},d["default"].createElement(N["default"],{baseUrl:e,pages:t,profile:this.props.profile,toggleNav:this.toggleNav,toggleModeration:this.toggleModeration,user:this.props.user}),d["default"].createElement("div",{className:"container"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm"},d["default"].createElement("div",{className:"profile-side-avatar"},d["default"].createElement(C["default"],{user:this.props.profile,size:"400"})),d["default"].createElement(P.SideNav,{baseUrl:e,pages:t,profile:this.props.profile})),d["default"].createElement("div",{className:"col-md-9"},this.props.children))))}}]),t}(M["default"]);a["default"]=B;var H={posts:v.Posts,threads:v.Threads,followers:_["default"],follows:E["default"],details:b["default"],"username-history":O["default"],"ban-details":m["default"]}},{"../..":301,"../../reducers/profile":355,"../../services/polls":373,"../../services/store":376,"../avatar":6,"../with-dropdown":298,"./ban-details":173,"./details":181,"./feed":182,"./followers":185,"./follows":186,"./header":187,"./moderation/nav":192,"./navs":193,"./username-history":195,react:"react","react-redux":"react-redux"}],195:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../button"),d=n(c),f=e("../quick-search"),p=n(f),m=e("../username-history/root"),h=n(m),b=e("../../index"),v=n(b),g=e("../../reducers/username-history"),_=e("../../services/ajax"),y=n(_),E=e("../../services/snackbar"),w=n(E),O=e("../../services/store"),k=n(O),N=e("../../services/page-title"),x=n(N),P=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.loadMore=function(){a.setState({isBusy:!0}),a.loadChanges(a.state.page+1,a.state.search)},a.search=function(e){a.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),a.loadChanges(1,e.target.value)},v["default"].has("PROFILE_NAME_HISTORY")?a.initWithPreloadedData(v["default"].pop("PROFILE_NAME_HISTORY")):a.initWithoutPreloadedData(),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},k["default"].dispatch((0,g.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadChanges()}},{key:"loadChanges",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;y["default"].get(v["default"].get("USERNAME_CHANGES_API"),{user:this.props.profile.id,search:a,page:t||1},"search-username-history").then(function(a){1===t?k["default"].dispatch((0,g.hydrate)(a.results)):k["default"].dispatch((0,g.append)(a.results)),e.setState({isLoaded:!0,isBusy:!1,count:a.count,more:a.more,page:a.page,pages:a.pages})},function(e){w["default"].apiError(e)})}},{key:"componentDidMount",value:function(){x["default"].set({title:gettext("Username history"),parent:this.props.profile.username})}},{key:"getLabel",value:function(){if(this.state.isLoaded){if(this.state.search){var e=ngettext("Found %(changes)s username change.","Found %(changes)s username changes.",this.state.count);return interpolate(e,{changes:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){var t=ngettext("Your username was changed %(changes)s time.","Your username was changed %(changes)s times.",this.state.count);return interpolate(t,{changes:this.state.count},!0)}var a=ngettext("%(username)s's username was changed %(changes)s time.","%(username)s's username was changed %(changes)s times.",this.state.count);return interpolate(a,{username:this.props.profile.username,changes:this.state.count},!0)}return gettext("Loading...")}},{key:"getEmptyMessage",value:function(){return this.state.search?gettext("Search returned no username changes matching specified criteria."):this.props.user.id===this.props.profile.id?gettext("No name changes have been recorded for your account."):interpolate(gettext("%(username)s's username was never changed."),{username:this.props.profile.username},!0)}},{key:"getMoreButton",value:function(){return this.state.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(d["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},interpolate(gettext("Show older (%(more)s)"),{more:this.state.more},!0))):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"profile-username-history"},u["default"].createElement("nav",{className:"toolbar"},u["default"].createElement("h3",{className:"toolbar-left"},this.getLabel()),u["default"].createElement(p["default"],{className:"toolbar-right",value:this.state.search,onChange:this.search,placeholder:gettext("Search history...")})),u["default"].createElement(h["default"],{isLoaded:this.state.isLoaded,emptyMessage:this.getEmptyMessage(),changes:this.props["username-history"]}),this.getMoreButton())}}]),t}(u["default"].Component);a["default"]=P},{"../../index":301,"../../reducers/username-history":362,"../../services/ajax":364,"../../services/page-title":372,"../../services/snackbar":375,"../../services/store":376,"../button":8,"../quick-search":196,"../username-history/root":279,react:"react"}],196:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.className?"form-search "+this.props.className:"form-search"}},{key:"render",value:function(){return u["default"].createElement("div",{className:this.getClassName()},u["default"].createElement("input",{type:"text",className:"form-control",value:this.props.value,onChange:this.props.onChange,placeholder:this.props.placeholder||gettext("Search...")}),u["default"].createElement("span",{className:"material-icon"},"search"))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],197:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./loader"),d=n(c),f=e("./register.js"),p=n(f),m=e("../services/ajax"),h=n(m),b=e("../services/captcha"),v=n(b),g=e("../services/modal"),_=n(g),y=e("../services/snackbar"),E=n(y),w=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.showRegisterForm=function(){"closed"===misago.get("SETTINGS").account_activation?E["default"].info(gettext("New registrations are currently disabled.")):a.state.isLoaded?_["default"].show(u["default"].createElement(p["default"],{criteria:a.state.criteria})):(a.setState({isLoading:!0}),Promise.all([v["default"].load(),h["default"].get(misago.get("AUTH_CRITERIA_API"))]).then(function(e){a.setState({isLoading:!1,isLoaded:!0,criteria:e[1]}),_["default"].show(u["default"].createElement(p["default"],{criteria:e[1]}))},function(){a.setState({isLoading:!1}),E["default"].error(gettext("Registration is currently unavailable due to an error."))}))},a.state={isLoading:!1,isLoaded:!1,criteria:null},a}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.className+(this.state.isLoading?" btn-loading":"")}},{key:"render",value:function(){return u["default"].createElement("button",{className:"btn "+this.getClassName(),disabled:this.state.isLoading,onClick:this.showRegisterForm,type:"button"},gettext("Register"),this.state.isLoading?u["default"].createElement(d["default"],null):null)}}]),t}(u["default"].Component);a["default"]=w},{"../services/ajax":364,"../services/captcha":366,"../services/modal":370,"../services/snackbar":375,"./loader":57,"./register.js":198,react:"react"}],198:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function s(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.RegisterComplete=a.RegisterForm=void 0;var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),d=e("react"),f=r(d),p=e("./button"),m=r(p),h=e("./form"),b=r(h),v=e("./form-group"),g=r(v),_=e("./password-strength"),y=r(_),E=e("./RegisterLegalFootnote"),w=r(E),O=e("./StartSocialAuth"),k=r(O),N=e(".."),x=r(N),P=e("../services/ajax"),j=r(P),C=e("../services/auth"),S=r(C),M=e("../services/captcha"),T=r(M),L=e("../services/modal"),A=r(L),R=e("../services/snackbar"),I=r(R),D=e("../utils/banned-page"),U=r(D),B=e("../utils/validators"),H=n(B),z=a.RegisterForm=function(e){function t(e){l(this,t);var a=s(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.handlePrivacyPolicyChange=function(e){var t=e.target.value;a.handleToggleAgreement("privacyPolicy",t)},a.handleTermsOfServiceChange=function(e){var t=e.target.value;a.handleToggleAgreement("termsOfService",t)},a.handleToggleAgreement=function(e,t){a.setState(function(n,r){if(null===n[e]){var l=u({},n.errors,o({},e,null));return o({errors:l},e,t)}var s=a.state.validators[e][0],i=u({},n.errors,o({},e,[s(null)]));return o({errors:i},e,null)})};var n=a.props.criteria,r=n.username,i=n.password,c=0;i.forEach(function(e){"MinimumLengthValidator"===e.name&&(c=e.min_length)});var d={username:[H.usernameContent(),H.usernameMinLength(r.min_length),H.usernameMaxLength(r.max_length)],email:[H.email()],password:[H.passwordMinLength(c)],captcha:T["default"].validator()};return x["default"].get("TERMS_OF_SERVICE_ID")&&(d.termsOfService=[H.requiredTermsOfService()]),x["default"].get("PRIVACY_POLICY_ID")&&(d.privacyPolicy=[H.requiredPrivacyPolicy()]),a.state={isLoading:!1,username:"",email:"",password:"",captcha:"",termsOfService:null,privacyPolicy:null,validators:d,errors:{}},a}return i(t,e),c(t,[{key:"clean",value:function(){return!!this.isValid()||(I["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return j["default"].post(x["default"].get("USERS_API"),{username:this.state.username,email:this.state.email,password:this.state.password,captcha:this.state.captcha,terms_of_service:this.state.termsOfService,privacy_policy:this.state.privacyPolicy})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),e.__all__&&e.__all__.length>0?I["default"].error(e.__all__[0]):I["default"].error(gettext("Form contains errors."))):403===e.status&&e.ban?((0,U["default"])(e.ban),A["default"].hide()):I["default"].apiError(e)}},{key:"render",value:function(){return f["default"].createElement("div",{className:"modal-dialog modal-register",role:"document"},f["default"].createElement("div",{className:"modal-content"},f["default"].createElement("div",{className:"modal-header"},f["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},f["default"].createElement("span",{"aria-hidden":"true"},"×")),f["default"].createElement("h4",{className:"modal-title"},gettext("Register"))),f["default"].createElement("form",{onSubmit:this.handleSubmit},f["default"].createElement("input",{type:"type",style:{display:"none"}}),f["default"].createElement("input",{type:"password",style:{display:"none"}}),f["default"].createElement("div",{className:"modal-body"},f["default"].createElement(k["default"],{
+buttonClassName:"col-xs-12 col-sm-6",buttonLabel:gettext("Join with %(site)s"),formLabel:gettext("Or create forum account:")}),f["default"].createElement(g["default"],{label:gettext("Username"),"for":"id_username",validation:this.state.errors.username},f["default"].createElement("input",{type:"text",id:"id_username",className:"form-control","aria-describedby":"id_username_status",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username})),f["default"].createElement(g["default"],{label:gettext("E-mail"),"for":"id_email",validation:this.state.errors.email},f["default"].createElement("input",{type:"text",id:"id_email",className:"form-control","aria-describedby":"id_email_status",disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email})),f["default"].createElement(g["default"],{label:gettext("Password"),"for":"id_password",validation:this.state.errors.password,extra:f["default"].createElement(y["default"],{password:this.state.password,inputs:[this.state.username,this.state.email]})},f["default"].createElement("input",{type:"password",id:"id_password",className:"form-control","aria-describedby":"id_password_status",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password})),T["default"].component({form:this}),f["default"].createElement(w["default"],{errors:this.state.errors,privacyPolicy:this.state.privacyPolicy,termsOfService:this.state.termsOfService,onPrivacyPolicyChange:this.handlePrivacyPolicyChange,onTermsOfServiceChange:this.handleTermsOfServiceChange})),f["default"].createElement("div",{className:"modal-footer"},f["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),f["default"].createElement(m["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Register account"))))))}}]),t}(b["default"]),F=a.RegisterComplete=function(e){function t(){return l(this,t),s(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return i(t,e),c(t,[{key:"getLead",value:function(){return"user"===this.props.activation?gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in."):"admin"===this.props.activation?gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."):void 0}},{key:"getSubscript",value:function(){return"user"===this.props.activation?gettext("We have sent an e-mail to %(email)s with link that you have to click to activate your account."):"admin"===this.props.activation?gettext("We will send an e-mail to %(email)s when this takes place."):void 0}},{key:"render",value:function(){return f["default"].createElement("div",{className:"modal-dialog modal-message modal-register",role:"document"},f["default"].createElement("div",{className:"modal-content"},f["default"].createElement("div",{className:"modal-header"},f["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},f["default"].createElement("span",{"aria-hidden":"true"},"×")),f["default"].createElement("h4",{className:"modal-title"},gettext("Registration complete"))),f["default"].createElement("div",{className:"modal-body"},f["default"].createElement("div",{className:"message-icon"},f["default"].createElement("span",{className:"material-icon"},"info_outline")),f["default"].createElement("div",{className:"message-body"},f["default"].createElement("p",{className:"lead"},interpolate(this.getLead(),{username:this.props.username},!0)),f["default"].createElement("p",null,interpolate(this.getSubscript(),{email:this.props.email},!0)),f["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))))}}]),t}(f["default"].Component),q=function(e){function t(e){l(this,t);var a=s(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.completeRegistration=function(e){"active"===e.activation?(A["default"].hide(),S["default"].signIn(e)):a.setState({complete:e})},a.state={complete:!1},a}return i(t,e),c(t,[{key:"render",value:function(){return this.state.complete?f["default"].createElement(F,{activation:this.state.complete.activation,email:this.state.complete.email,username:this.state.complete.username}):f["default"].createElement(z,u({callback:this.completeRegistration},this.props))}}]),t}(f["default"].Component);a["default"]=q},{"..":301,"../services/ajax":364,"../services/auth":365,"../services/captcha":366,"../services/modal":370,"../services/snackbar":375,"../utils/banned-page":378,"../utils/validators":392,"./RegisterLegalFootnote":1,"./StartSocialAuth":2,"./button":8,"./form":55,"./form-group":54,"./password-strength":102,react:"react"}],199:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LinkSent=a.RequestLinkForm=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../index"),f=r(d),p=e("./button"),m=r(p),h=e("./form"),b=r(h),v=e("../services/ajax"),g=r(v),_=e("../services/snackbar"),y=r(_),E=e("../utils/validators"),w=n(E),O=e("../utils/banned-page"),k=r(O),N=a.RequestLinkForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,email:"",validators:{email:[w.email()]}},a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(y["default"].error(gettext("Enter a valid email address.")),!1)}},{key:"send",value:function(){return g["default"].post(f["default"].get("SEND_ACTIVATION_API"),{email:this.state.email})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){["already_active","inactive_admin"].indexOf(e.code)>-1?y["default"].info(e.detail):403===e.status&&e.ban?(0,k["default"])(e.ban):y["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-activation-link"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"form-group"},c["default"].createElement("div",{className:"control-input"},c["default"].createElement("input",{type:"text",className:"form-control",placeholder:gettext("Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),c["default"].createElement(m["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Send link"))))}}]),t}(b["default"]),x=a.LinkSent=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getMessage",value:function(){return interpolate(gettext("Activation link was sent to %(email)s"),{email:this.props.user.email},!0)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-activation-link well-done"},c["default"].createElement("div",{className:"done-message"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"check")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",null,this.getMessage())),c["default"].createElement("button",{className:"btn btn-primary btn-block",type:"button",onClick:this.props.callback},gettext("Request another link"))))}}]),t}(c["default"].Component),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.complete=function(e){a.setState({complete:e})},a.reset=function(){a.setState({complete:!1})},a.state={complete:!1},a}return s(t,e),i(t,[{key:"render",value:function(){return this.state.complete?c["default"].createElement(x,{user:this.state.complete,callback:this.reset}):c["default"].createElement(N,{callback:this.complete})}}]),t}(c["default"].Component);a["default"]=P},{"../index":301,"../services/ajax":364,"../services/snackbar":375,"../utils/banned-page":378,"../utils/validators":392,"./button":8,"./form":55,react:"react"}],200:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.AccountInactivePage=a.LinkSent=a.RequestResetForm=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("react-dom"),f=r(d),p=e("../index"),m=r(p),h=e("./button"),b=r(h),v=e("./form"),g=r(v),_=e("../services/ajax"),y=r(_),E=e("../services/snackbar"),w=r(E),O=e("../utils/validators"),k=n(O),N=e("../utils/banned-page"),x=r(N),P=a.RequestResetForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,email:"",validators:{email:[k.email()]}},a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(w["default"].error(gettext("Enter a valid email address.")),!1)}},{key:"send",value:function(){return y["default"].post(m["default"].get("SEND_PASSWORD_RESET_API"),{email:this.state.email})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){["inactive_user","inactive_admin"].indexOf(e.code)>-1?this.props.showInactivePage(e):403===e.status&&e.ban?(0,x["default"])(e.ban):w["default"].apiError(e)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-password-reset"},c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"form-group"},c["default"].createElement("div",{className:"control-input"},c["default"].createElement("input",{type:"text",className:"form-control",placeholder:gettext("Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),c["default"].createElement(b["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Send link"))))}}]),t}(g["default"]),j=a.LinkSent=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getMessage",value:function(){return interpolate(gettext("Reset password link was sent to %(email)s"),{email:this.props.user.email},!0)}},{key:"render",value:function(){return c["default"].createElement("div",{className:"well well-form well-form-request-password-reset well-done"},c["default"].createElement("div",{className:"done-message"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"check")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",null,this.getMessage())),c["default"].createElement("button",{type:"button",className:"btn btn-primary btn-block",onClick:this.props.callback},gettext("Request another link"))))}}]),t}(c["default"].Component),C=a.AccountInactivePage=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getActivateButton",value:function(){return"inactive_user"===this.props.activation?c["default"].createElement("p",null,c["default"].createElement("a",{href:m["default"].get("REQUEST_ACTIVATION_URL")},gettext("Activate your account."))):null}},{key:"render",value:function(){return c["default"].createElement("div",{className:"page page-message page-message-info page-forgotten-password-inactive"},c["default"].createElement("div",{className:"container"},c["default"].createElement("div",{className:"message-panel"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("Your account is inactive.")),c["default"].createElement("p",null,this.props.message),this.getActivateButton()))))}}]),t}(c["default"].Component),S=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.complete=function(e){a.setState({complete:e})},a.reset=function(){a.setState({complete:!1})},a.state={complete:!1},a}return s(t,e),i(t,[{key:"showInactivePage",value:function(e){f["default"].render(c["default"].createElement(C,{activation:e.code,message:e.detail}),document.getElementById("page-mount"))}},{key:"render",value:function(){return this.state.complete?c["default"].createElement(j,{callback:this.reset,user:this.state.complete}):c["default"].createElement(P,{callback:this.complete,showInactivePage:this.showInactivePage})}}]),t}(c["default"].Component);a["default"]=S},{"../index":301,"../services/ajax":364,"../services/snackbar":375,"../utils/banned-page":378,"../utils/validators":392,"./button":8,"./form":55,react:"react","react-dom":"react-dom"}],201:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.PasswordChangedPage=a.ResetPasswordForm=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-dom"),d=n(c),f=e("../index"),p=n(f),m=e("./button"),h=n(m),b=e("./form"),v=n(b),g=e("./sign-in.js"),_=n(g),y=e("../services/ajax"),E=n(y),w=e("../services/auth"),O=n(w),k=e("../services/modal"),N=n(k),x=e("../services/snackbar"),P=n(x),j=e("../utils/banned-page"),C=n(j),S=a.ResetPasswordForm=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,password:""},a}return l(t,e),s(t,[{key:"clean",value:function(){return!!this.state.password.trim().length||(P["default"].error(gettext("Enter new password.")),!1)}},{key:"send",value:function(){return E["default"].post(p["default"].get("CHANGE_PASSWORD_API"),{password:this.state.password})}},{key:"handleSuccess",value:function(e){this.props.callback(e)}},{key:"handleError",value:function(e){403===e.status&&e.ban?(0,C["default"])(e.ban):P["default"].apiError(e)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"well well-form well-form-reset-password"},u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{type:"password",className:"form-control",placeholder:gettext("Enter new password"),disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),u["default"].createElement(h["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Change password"))))}}]),t}(v["default"]),M=a.PasswordChangedPage=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getMessage",value:function(){return interpolate(gettext("%(username)s, your password has been changed successfully."),{username:this.props.user.username},!0)}},{key:"showSignIn",value:function(){N["default"].show(_["default"])}},{key:"render",value:function(){return u["default"].createElement("div",{className:"page page-message page-message-success page-forgotten-password-changed"},u["default"].createElement("div",{className:"container"},u["default"].createElement("div",{className:"message-panel"},u["default"].createElement("div",{className:"message-icon"},u["default"].createElement("span",{className:"material-icon"},"check")),u["default"].createElement("div",{className:"message-body"},u["default"].createElement("p",{className:"lead"},this.getMessage()),u["default"].createElement("p",null,gettext("You will have to sign in using new password before continuing.")),u["default"].createElement("p",null,u["default"].createElement("button",{type:"button",className:"btn btn-primary",onClick:this.showSignIn},gettext("Sign in")))))))}}]),t}(u["default"].Component),T=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.complete=function(e){O["default"].softSignOut(),$('#hidden-login-form input[name="redirect_to"]').remove(),d["default"].render(u["default"].createElement(M,{user:e}),document.getElementById("page-mount"))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement(S,{callback:this.complete})}}]),t}(u["default"].Component);a["default"]=T},{"../index":301,"../services/ajax":364,"../services/auth":365,"../services/modal":370,"../services/snackbar":375,"../utils/banned-page":378,"./button":8,"./form":55,"./sign-in.js":210,react:"react","react-dom":"react-dom"}],202:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../.."),d=n(c),f=e("../form"),p=n(f),m=e("../../reducers/posts"),h=e("../../reducers/search"),b=e("../../reducers/users"),v=e("../../services/ajax"),g=n(v),_=e("../../services/snackbar"),y=n(_),E=e("../../services/store"),w=n(E),O=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onQueryChange=function(e){a.changeValue("query",e.target.value)},a.state={isLoading:!1,query:e.search.query},a}return l(t,e),s(t,[{key:"componentDidMount",value:function(){this.state.query.length&&this.handleSubmit()}},{key:"clean",value:function(){return!!this.state.query.trim().length||(y["default"].error(gettext("You have to enter search query.")),!1)}},{key:"send",value:function(){return w["default"].dispatch((0,h.update)({isLoading:!0})),g["default"].get(d["default"].get("SEARCH_API"),{q:this.state.query.trim()})}},{key:"handleSuccess",value:function(e){w["default"].dispatch((0,h.update)({query:this.state.query.trim(),isLoading:!1,providers:e})),e.forEach(function(e){"users"===e.id?w["default"].dispatch((0,b.hydrate)(e.results.results)):"threads"===e.id&&w["default"].dispatch((0,m.load)(e.results))})}},{key:"handleError",value:function(e){y["default"].apiError(e),w["default"].dispatch((0,h.update)({isLoading:!1}))}},{key:"render",value:function(){return u["default"].createElement("div",{className:"page-header-bg"},u["default"].createElement("div",{className:"page-header page-search-form"},u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"container"},u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-xs-12 col-md-3"},u["default"].createElement("h1",null,gettext("Search"))),u["default"].createElement("div",{className:"col-xs-12 col-md-9"},u["default"].createElement("div",{className:"row xs-margin-top sm-margin-top"},u["default"].createElement("div",{className:"col-xs-12 col-sm-8 col-md-9"},u["default"].createElement("div",{className:"form-group"},u["default"].createElement("input",{className:"form-control",disabled:this.props.search.isLoading||this.state.isLoading,onChange:this.onQueryChange,type:"text",value:this.state.query}))),u["default"].createElement("div",{className:"col-xs-12 col-sm-4 col-md-3"},u["default"].createElement("button",{className:"btn btn-primary btn-block btn-outline",disabled:this.props.search.isLoading||this.state.isLoading},gettext("Search"))))))))))}}]),t}(p["default"]);a["default"]=O},{"../..":301,"../../reducers/posts":353,"../../reducers/search":356,"../../reducers/users":363,"../../services/ajax":364,"../../services/snackbar":375,"../../services/store":376,"../form":55,react:"react"}],203:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{posts:e.posts,search:e.search,tick:e.tick.tick,user:e.auth.user,users:e.users}}Object.defineProperty(a,"__esModule",{value:!0}),a.select=r,a["default"]=function(e){return e.map(function(e){return{path:e.url,component:(0,o.connect)(r)(c[e.id]),provider:e}})};var o=e("react-redux"),l=e("./threads"),s=n(l),i=e("./users"),u=n(i),c={threads:s["default"],users:u["default"]}},{"./threads":206,"./users":208,"react-redux":"react-redux"}],204:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=null;if(e.search.providers.forEach(function(a){a.id===e.provider.id&&(t=a.time)}),null===t)return null;var a=gettext("Search took %(time)s s to complete");return l["default"].createElement("footer",{className:"search-footer"},l["default"].createElement("p",null,interpolate(a,{time:t},!0)))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("div",{className:"page page-search"},l["default"].createElement(i["default"],{provider:e.provider,search:e.search}),l["default"].createElement("div",{className:"container"},l["default"].createElement("div",{className:"row"},l["default"].createElement("div",{className:"col-md-3"},l["default"].createElement(c["default"],{providers:e.search.providers})),l["default"].createElement("div",{className:"col-md-9"},e.children,l["default"].createElement(r,{provider:e.provider,search:e.search})))))},a.SearchTime=r;var o=e("react"),l=n(o),s=e("./form"),i=n(s),u=e("./sidenav"),c=n(u)},{"./form":202,"./sidenav":205,react:"react"}],205:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if(!e.results)return null;var t=e.results.count;return t>1e6?t=Math.ceil(t/1e6)+"KK":t>1e3&&(t=Math.ceil(t/1e3)+"K"),l["default"].createElement("span",{className:"badge"},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement("div",{className:"list-group nav-side"},e.providers.map(function(e){return l["default"].createElement(s.Link,{activeClassName:"active",className:"list-group-item",key:e.id,to:e.url},l["default"].createElement("span",{className:"material-icon"},e.icon),e.name,l["default"].createElement(r,{results:e.results}))}))},a.Badge=r;var o=e("react"),l=n(o),s=e("react-router")},{react:"react","react-router":"react-router"}],206:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.posts&&e.posts.count?e.children:e.query.length?s["default"].createElement("p",{className:"lead"},gettext("No threads matching search query have been found.")):s["default"].createElement("p",{className:"lead"},gettext("Enter at least two characters to search threads."))}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return s["default"].createElement(u["default"],{provider:e.route.provider,search:e.search},s["default"].createElement(r,{query:e.search.query,posts:e.posts},s["default"].createElement(d["default"],o({provider:e.route.provider,query:e.search.query},e.posts))))},a.Blankslate=r;var l=e("react"),s=n(l),i=e("../page"),u=n(i),c=e("./results"),d=n(c)},{"../page":204,"./results":207,react:"react"}],207:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.LoadMore=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return u["default"].createElement("div",null,u["default"].createElement(d["default"],{isReady:!0,posts:e.results}),u["default"].createElement(O,e))};var i=e("react"),u=n(i),c=e("../../post-feed"),d=n(c),f=e("../../button"),p=n(f),m=e("../../misago-markup"),h=(n(m),e("../../../reducers/posts")),b=e("../../../reducers/search"),v=e("../../../services/ajax"),g=n(v),_=e("../../../services/snackbar"),y=n(_),E=e("../../../services/store"),w=n(E),O=a.LoadMore=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){w["default"].dispatch((0,h.update)({isBusy:!0})),g["default"].get(n.props.provider.api,{q:n.props.query,page:n.props.next}).then(function(e){e.forEach(function(e){"threads"===e.id&&(w["default"].dispatch((0,h.append)(e.results)),w["default"].dispatch((0,b.updateProvider)(e)))}),w["default"].dispatch((0,h.update)({isBusy:!1}))},function(e){y["default"].apiError(e),w["default"].dispatch((0,h.update)({isBusy:!1}))})},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){return this.props.more?u["default"].createElement("div",{className:"pager-more"},u["default"].createElement(p["default"],{className:"btn btn-default btn-outline",loading:this.props.isBusy,onClick:this.onClick},gettext("Show more"))):null}}]),t}(u["default"].Component)},{"../../../reducers/posts":353,"../../../reducers/search":356,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,"../../button":8,"../../misago-markup":59,"../../post-feed":121,react:"react"}],208:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.users.length?e.children:e.query.length?l["default"].createElement("p",{className:"lead"},gettext("No users matching search query have been found.")):l["default"].createElement("p",{className:"lead"},gettext("Enter at least two characters to search users."))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return l["default"].createElement(i["default"],{provider:e.route.provider,search:e.search},l["default"].createElement(r,{query:e.search.query,users:e.users},l["default"].createElement(c["default"],{cols:3,isReady:!0,users:e.users})))},a.Blankslate=r;var o=e("react"),l=n(o),s=e("../page"),i=n(s),u=e("../../users-list"),c=n(u)},{"../../users-list":283,"../page":204,react:"react"}],209:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.icon;return t?c["default"].createElement("span",{className:"material-icon"},t):null}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Icon=s;var u=e("react"),c=n(u),d=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.change=function(e){return function(){n.props.onChange({target:{value:e}})}},l=a,o(n,l)}return l(t,e),i(t,[{key:"getChoice",value:function(){var e=this,t=null;return this.props.choices.map(function(a){a.value===e.props.value&&(t=a)}),t}},{key:"getIcon",value:function(){return this.getChoice().icon}},{key:"getLabel",value:function(){return this.getChoice().label}},{key:"render",value:function(){var e=this;return c["default"].createElement("div",{className:"btn-group btn-select-group"},c["default"].createElement("button",{
+type:"button",className:"btn btn-select dropdown-toggle",id:this.props.id||null,"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false","aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},c["default"].createElement(s,{icon:this.getIcon()}),this.getLabel()),c["default"].createElement("ul",{className:"dropdown-menu"},this.props.choices.map(function(t,a){return c["default"].createElement("li",{key:a},c["default"].createElement("button",{type:"button",className:"btn-link",onClick:e.change(t.value)},c["default"].createElement(s,{icon:t.icon}),t.label))})))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],210:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../index"),d=n(c),f=e("./button"),p=n(f),m=e("./form"),h=n(m),b=e("./StartSocialAuth"),v=n(b),g=e("../services/ajax"),_=n(g),y=e("../services/modal"),E=n(y),w=e("../services/snackbar"),O=n(w),k=e("../utils/banned-page"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoading:!1,showActivation:!1,username:"",password:"",validators:{username:[],password:[]}},a}return l(t,e),s(t,[{key:"clean",value:function(){return!!this.isValid()||(O["default"].error(gettext("Fill out both fields.")),!1)}},{key:"send",value:function(){return _["default"].post(d["default"].get("AUTH_API"),{username:this.state.username,password:this.state.password})}},{key:"handleSuccess",value:function(){var e=$("#hidden-login-form");e.append('<input type="text" name="username" />'),e.append('<input type="password" name="password" />'),e.find('input[type="hidden"]').val(_["default"].getCsrfToken()),e.find('input[name="redirect_to"]').val(window.location.pathname),e.find('input[name="username"]').val(this.state.username),e.find('input[name="password"]').val(this.state.password),e.submit(),this.setState({isLoading:!0})}},{key:"handleError",value:function(e){400===e.status?"inactive_admin"===e.code?O["default"].info(e.detail):"inactive_user"===e.code?(O["default"].info(e.detail),this.setState({showActivation:!0})):"banned"===e.code?((0,N["default"])(e.detail),E["default"].hide()):O["default"].error(e.detail):403===e.status&&e.ban?((0,N["default"])(e.ban),E["default"].hide()):O["default"].apiError(e)}},{key:"getActivationButton",value:function(){return this.state.showActivation?u["default"].createElement("a",{className:"btn btn-success btn-block",href:d["default"].get("REQUEST_ACTIVATION_URL")},gettext("Activate account")):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-dialog modal-sm modal-sign-in",role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Sign in"))),u["default"].createElement("form",{onSubmit:this.handleSubmit},u["default"].createElement("div",{className:"modal-body"},u["default"].createElement(v["default"],{buttonLabel:gettext("Sign in with %(site)s"),formLabel:gettext("Or use your forum account:"),labelClassName:"text-center"}),u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_username",onChange:this.bindInput("username"),placeholder:gettext("Username or e-mail"),type:"text",value:this.state.username}))),u["default"].createElement("div",{className:"form-group"},u["default"].createElement("div",{className:"control-input"},u["default"].createElement("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_password",onChange:this.bindInput("password"),placeholder:gettext("Password"),type:"password",value:this.state.password})))),u["default"].createElement("div",{className:"modal-footer"},this.getActivationButton(),u["default"].createElement(p["default"],{className:"btn-primary btn-block",loading:this.state.isLoading},gettext("Sign in")),u["default"].createElement("a",{className:"btn btn-default btn-block",href:d["default"].get("FORGOTTEN_PASSWORD_URL")},gettext("Forgot password?"))))))}}]),t}(h["default"]);a["default"]=x},{"../index":301,"../services/ajax":364,"../services/modal":370,"../services/snackbar":375,"../utils/banned-page":378,"./StartSocialAuth":2,"./button":8,"./form":55,react:"react"}],211:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.snackbar}Object.defineProperty(a,"__esModule",{value:!0}),a.Snackbar=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d={info:"alert-info",success:"alert-success",warning:"alert-warning",error:"alert-danger"};a.Snackbar=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getSnackbarClass",value:function(){var e="alerts-snackbar";return e+=this.props.isVisible?" in":" out"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getSnackbarClass()},c["default"].createElement("p",{className:"alert "+d[this.props.type]},this.props.message))}}]),t}(c["default"].Component)},{react:"react"}],212:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=e("./header"),s=n(l),i=e("../.."),u=n(i),c=function(e){var t=e.activation,a=e.backend_name,n=e.username,r="",l="";return l="user"===t?gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in."):"admin"===t?gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."):gettext("%(username)s, your account has been created and you have been signed in to it."),r="active"===t?"check":"info_outline",o["default"].createElement("div",{className:"page page-social-auth page-social-sauth-register"},o["default"].createElement(s["default"],{backendName:a}),o["default"].createElement("div",{className:"container"},o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},o["default"].createElement("div",{className:"panel panel-default panel-form"},o["default"].createElement("div",{className:"panel-heading"},o["default"].createElement("h3",{className:"panel-title"},gettext("Registration completed!"))),o["default"].createElement("div",{className:"panel-body panel-message-body"},o["default"].createElement("div",{className:"message-icon"},o["default"].createElement("span",{className:"material-icon"},r)),o["default"].createElement("div",{className:"message-body"},o["default"].createElement("p",{className:"lead"},interpolate(l,{username:n},!0)),o["default"].createElement("p",{className:"help-block"},o["default"].createElement("a",{className:"btn btn-default",href:u["default"].get("MISAGO_PATH")},gettext("Return to forum index"))))))))))};a["default"]=c},{"../..":301,"./header":213,react:"react"}],213:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0});var r=e("react"),o=n(r),l=function(e){var t=e.backendName,a=gettext("Sign in with %(backend)s"),n=interpolate(a,{backend:t},!0);return o["default"].createElement("div",{className:"page-header-bg"},o["default"].createElement("div",{className:"page-header"},o["default"].createElement("div",{className:"container"},o["default"].createElement("h1",null,n))))};a["default"]=l},{react:"react"}],214:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./register"),d=n(c),f=e("./complete"),p=n(f),m=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleRegistrationComplete=function(e){var t=e.activation,n=e.email,r=e.step,o=e.username;a.setState({activation:t,email:n,step:r,username:o})},a.state={step:e.step,activation:e.activation||"",email:e.email||"",username:e.username||""},a}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props,t=e.backend_name,a=e.url,n=this.state,r=n.activation,o=n.email,l=n.step,s=n.username;return"register"===l?u["default"].createElement(d["default"],{backend_name:t,email:o,url:a,username:s,onRegistrationComplete:this.handleRegistrationComplete}):u["default"].createElement(p["default"],{activation:r,backend_name:t,email:o,url:a,username:s})}}]),t}(u["default"].Component);a["default"]=m},{"./complete":212,"./register":215,react:"react"}],215:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function s(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),d=e("react"),f=r(d),p=e("../.."),m=r(p),h=e("../RegisterLegalFootnote"),b=r(h),v=e("../button"),g=r(v),_=e("../form"),y=r(_),E=e("../form-group"),w=r(E),O=e("../../services/ajax"),k=r(O),N=e("../../services/snackbar"),x=r(N),P=e("../../utils/validators"),j=n(P),C=e("./header"),S=r(C),M=function(e){function t(e){l(this,t);var a=s(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.handlePrivacyPolicyChange=function(e){var t=e.target.value;a.handleToggleAgreement("privacyPolicy",t)},a.handleTermsOfServiceChange=function(e){var t=e.target.value;a.handleToggleAgreement("termsOfService",t)},a.handleToggleAgreement=function(e,t){a.setState(function(n,r){if(null===n[e]){var l=u({},n.errors,o({},e,null));return o({errors:l},e,t)}var s=a.state.validators[e][0],i=u({},n.errors,o({},e,[s(null)]));return o({errors:i},e,null)})};var n={email:[j.email()],username:[j.usernameContent()]};return m["default"].get("TERMS_OF_SERVICE_ID")&&(n.termsOfService=[j.requiredTermsOfService()]),m["default"].get("PRIVACY_POLICY_ID")&&(n.privacyPolicy=[j.requiredPrivacyPolicy()]),a.state={email:e.email||"",emailProtected:!!e.email,username:e.username||"",termsOfService:null,privacyPolicy:null,validators:n,errors:{},isLoading:!1},a}return i(t,e),c(t,[{key:"clean",value:function(){var e=(this.validate(),[this.state.email.trim().length,this.state.username.trim().length]);if(e.indexOf(0)!==-1)return x["default"].error(gettext("Fill out all fields.")),!1;var t=this.state.validators,a=!!m["default"].get("TERMS_OF_SERVICE_ID");if(a&&null===this.state.termsOfService)return x["default"].error(t.termsOfService[0](null)),!1;var n=!!m["default"].get("PRIVACY_POLICY_ID");return!n||null!==this.state.privacyPolicy||(x["default"].error(t.privacyPolicy[0](null)),x["default"].error(gettext("You need to accept the privacy policy.")),!1)}},{key:"send",value:function(){return k["default"].post(this.props.url,{email:this.state.email,username:this.state.username,terms_of_service:this.state.termsOfService,privacy_policy:this.state.privacyPolicy})}},{key:"handleSuccess",value:function(e){onRegistrationComplete(e)}},{key:"handleError",value:function(e){if(200===e.status){var t=this.props.onRegistrationComplete,a=this.state.username;t({activation:"active",step:"done",username:a})}else if(400===e.status){var n={errors:e};e.email&&(n.emailProtected=!1),this.setState(n)}else x["default"].apiError(e)}},{key:"render",value:function(){var e=this.props.backend_name,t=this.state,a=t.email,n=t.emailProtected,r=t.username,o=t.isLoading,l=null;if(n){var s=gettext("Your e-mail address has been verified by %(backend)s.");l=interpolate(s,{backend:e},!0)}return f["default"].createElement("div",{className:"page page-social-auth page-social-sauth-register"},f["default"].createElement(S["default"],{backendName:e}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row"},f["default"].createElement("div",{className:"col-md-6 col-md-offset-3"},f["default"].createElement("form",{onSubmit:this.handleSubmit},f["default"].createElement("div",{className:"panel panel-default panel-form"},f["default"].createElement("div",{className:"panel-heading"},f["default"].createElement("h3",{className:"panel-title"},gettext("Complete your details"))),f["default"].createElement("div",{className:"panel-body"},f["default"].createElement(w["default"],{"for":"id_username",label:gettext("Username"),validation:this.state.errors.username},f["default"].createElement("input",{type:"text",id:"id_username",className:"form-control",disabled:o,onChange:this.bindInput("username"),value:r})),f["default"].createElement(w["default"],{"for":"id_email",label:gettext("E-mail address"),helpText:l,validation:n?null:this.state.errors.email},f["default"].createElement("input",{type:"email",id:"id_email",className:"form-control",disabled:o||n,onChange:this.bindInput("email"),value:a})),f["default"].createElement(b["default"],{errors:this.state.errors,privacyPolicy:this.state.privacyPolicy,termsOfService:this.state.termsOfService,onPrivacyPolicyChange:this.handlePrivacyPolicyChange,onTermsOfServiceChange:this.handleTermsOfServiceChange})),f["default"].createElement("div",{className:"panel-footer"},f["default"].createElement(g["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Sign in")))))))))}}]),t}(y["default"]);a["default"]=M},{"../..":301,"../../services/ajax":364,"../../services/snackbar":375,"../../utils/validators":392,"../RegisterLegalFootnote":1,"../button":8,"../form":55,"../form-group":54,"./header":213,react:"react"}],216:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return s["default"].createElement("li",null,s["default"].createElement("a",{href:e.node.url.index},e.node.name))}function o(e){var t=e.path[e.path.length-1];return s["default"].createElement("a",{href:t.url.index,className:"go-back-sm visible-xs-block"},s["default"].createElement("span",{className:"material-icon"},"chevron_left"),t.name)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return s["default"].createElement("div",{className:"page-breadcrumbs"},s["default"].createElement("div",{className:"container"},s["default"].createElement("ol",{className:"breadcrumb hidden-xs"},e.path.map(function(e){return s["default"].createElement(r,{key:e.id,node:e})})),s["default"].createElement(o,e)))},a.Breadcrumb=r,a.GoBack=o;var l=e("react"),s=n(l)},{react:"react"}],217:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return f["default"].createElement("div",{className:e.isSingle?"col-xs-12":"col-xs-6"},f["default"].createElement("div",{className:"btn-group btn-group-justified"},f["default"].createElement("div",{className:"btn-group"},f["default"].createElement("button",{"aria-expanded":"false","aria-haspopup":"true",className:"btn btn-default btn-outline dropdown-toggle","data-toggle":"dropdown",disabled:e.thread.isBusy,type:"button"},f["default"].createElement("span",{className:"material-icon"},"settings"),f["default"].createElement("span",{className:e.isSingle?"":"hidden-sm"},gettext("Moderation"))),f["default"].createElement(h.ModerationControls,{posts:e.posts,thread:e.thread,user:e.user}))))}Object.defineProperty(a,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Moderation=i;var d=e("react"),f=r(d),p=e("./breadcrumbs"),m=r(p),h=e("../moderation/thread"),b=e("./stats"),v=r(b),g=e("../../form"),_=r(g),y=e("../../posting/utils/validators"),E=e("../../../services/ajax"),w=r(E),O=e("../../../services/snackbar"),k=r(O),N=e("../../../services/store"),x=r(N),P=e("../../../reducers/thread"),j=n(P),C=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onChange=function(e){a.changeValue("title",e.target.value)},a.onEdit=function(){a.setState({isEditing:!0})},a.onCancel=function(){a.setState({title:a.props.thread.title,isEditing:!1})},a.state={isEditing:!1,isLoading:!1,title:e.thread.title,validators:{title:(0,y.getTitleValidators)()},errors:{}},a}return s(t,e),c(t,[{key:"clean",value:function(){if(!this.state.title.trim().length)return k["default"].error(gettext("You have to enter thread title.")),!1;var e=this.validate();return!e.title||(k["default"].error(e.title[0]),!1)}},{key:"send",value:function(){return w["default"].patch(this.props.thread.api.index,[{op:"replace",path:"title",value:this.state.title}])}},{key:"handleSuccess",value:function(e){x["default"].dispatch(j.update(e)),this.setState({isEditing:!1})}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail[0]):k["default"].apiError(e)}},{key:"render",value:function(){var e=this.props,t=e.thread,a=e.user,n=!!a.id&&(0,h.isModerationVisible)(t);return this.state.isEditing?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row xs-margin-top title-edit-form"},f["default"].createElement("form",{onSubmit:this.handleSubmit},f["default"].createElement("div",{className:"col-sm-6 col-md-6"},f["default"].createElement("input",{className:"form-control",type:"text",value:this.state.title,onChange:this.onChange})),f["default"].createElement("div",{className:"col-sm-6 col-md-4"},f["default"].createElement("div",{className:"row xs-margin-top-half sm-margin-top-no md-margin-top-no"},f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-primary btn-block btn-outline",disabled:this.state.isLoading,title:gettext("Change title")},gettext("Save changes"))),f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",disabled:this.state.isLoading,onClick:this.onCancel,title:gettext("Cancel"),type:"button"},gettext("Cancel")))))))),f["default"].createElement(v["default"],{thread:t})):a.id&&t.acl.can_edit?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row"},f["default"].createElement("div",{className:n?"col-sm-9 col-md-8":"col-sm-10 col-md-10"},f["default"].createElement("h1",null,t.title)),f["default"].createElement("div",{className:n?"col-sm-3 col-md-4":"col-sm-3 col-md-2"},f["default"].createElement("div",{className:"row xs-margin-top md-margin-top-no"},f["default"].createElement("div",{className:n?"col-xs-6":"col-xs-12"},f["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",onClick:this.onEdit,title:gettext("Edit title"),type:"button"},f["default"].createElement("span",{className:"material-icon"},"edit"),f["default"].createElement("span",{className:"hidden-sm"},gettext("Edit")))),n&&f["default"].createElement(i,this.props))))),f["default"].createElement(v["default"],{thread:t})):n?f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("div",{className:"row"},f["default"].createElement("div",{className:"col-sm-9 col-md-10"},f["default"].createElement("h1",null,t.title)),f["default"].createElement("div",{className:"col-sm-3 col-md-2"},f["default"].createElement("div",{className:"row xs-margin-top md-margin-top-no"},f["default"].createElement(i,u({isSingle:!0},this.props)))))),f["default"].createElement(v["default"],{thread:t})):f["default"].createElement("div",{className:"page-header"},f["default"].createElement(m["default"],{path:t.path}),f["default"].createElement("div",{className:"container"},f["default"].createElement("h1",null,t.title)),f["default"].createElement(v["default"],{thread:t}))}}]),t}(_["default"]);a["default"]=C},{"../../../reducers/thread":359,"../../../services/ajax":364,"../../../services/snackbar":375,"../../../services/store":376,"../../form":55,"../../posting/utils/validators":143,"../moderation/thread":226,"./breadcrumbs":216,"./stats":218,react:"react"}],218:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return 2==e.thread.weight?d["default"].createElement("li",{className:"thread-pinned-globally"},d["default"].createElement("span",{className:"material-icon"},"bookmark"),d["default"].createElement("span",{className:"icon-legend"},gettext("Pinned globally"))):1==e.thread.weight?d["default"].createElement("li",{className:"thread-pinned-locally"},d["default"].createElement("span",{className:"material-icon"},"bookmark_border"),d["default"].createElement("span",{className:"icon-legend"},gettext("Pinned locally"))):null}function o(e){return e.thread.is_unapproved?d["default"].createElement("li",{className:"thread-unapproved"},d["default"].createElement("span",{className:"material-icon"},"remove_circle"),d["default"].createElement("span",{className:"icon-legend"},gettext("Unapproved"))):e.thread.has_unapproved_posts?d["default"].createElement("li",{className:"thread-unapproved-posts"},d["default"].createElement("span",{className:"material-icon"},"remove_circle_outline"),d["default"].createElement("span",{className:"icon-legend"},gettext("Unapproved posts"))):null}function l(e){return e.thread.is_hidden?d["default"].createElement("li",{className:"thread-hidden"},d["default"].createElement("span",{className:"material-icon"},"visibility_off"),d["default"].createElement("span",{className:"icon-legend"},gettext("Hidden"))):null}function s(e){return e.thread.is_closed?d["default"].createElement("li",{className:"thread-closed"},d["default"].createElement("span",{className:"material-icon"},"lock_outline"),d["default"].createElement("span",{className:"icon-legend"},gettext("Closed"))):null}function i(e){var t=ngettext("%(replies)s reply","%(replies)s replies",e.thread.replies),a=interpolate(t,{replies:e.thread.replies},!0);return d["default"].createElement("li",{className:"thread-replies"},d["default"].createElement("span",{className:"material-icon"},"forum"),d["default"].createElement("span",{className:"icon-legend"},a))}function u(e){var t=null;t=e.thread.url.last_poster?interpolate(m,{url:(0,p["default"])(e.thread.url.last_poster),user:(0,p["default"])(e.thread.last_poster_name)},!0):interpolate(h,{user:(0,p["default"])(e.thread.last_poster_name)},!0);var a=interpolate(b,{absolute:(0,p["default"])(e.thread.last_post_on.format("LLL")),relative:(0,p["default"])(e.thread.last_post_on.fromNow())},!0),n=interpolate((0,p["default"])(gettext("last reply by %(user)s %(date)s")),{date:a,user:t},!0);return d["default"].createElement("li",{className:"thread-last-reply",dangerouslySetInnerHTML:{__html:n}})}Object.defineProperty(a,"__esModule",{value:!0}),a.Weight=r,a.Unapproved=o,a.IsHidden=l,a.IsClosed=s,a.Replies=i,a.LastReply=u,a["default"]=function(e){return d["default"].createElement("div",{className:"header-stats"},d["default"].createElement("div",{className:"container"},d["default"].createElement("ul",{className:"list-inline"},d["default"].createElement(r,{thread:e.thread}),d["default"].createElement(o,{thread:e.thread}),d["default"].createElement(l,{thread:e.thread}),d["default"].createElement(s,{thread:e.thread}),d["default"].createElement(i,{thread:e.thread}),d["default"].createElement(u,{thread:e.thread}))))};var c=e("react"),d=n(c),f=e("../../../utils/escape-html"),p=n(f),m='<a href="%(url)s" class="poster-title">%(user)s</a>',h='<span class="poster-title">%(user)s</span>',b='<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'},{"../../../utils/escape-html":382,react:"react"}],219:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){var t=e.selection,a=[{op:"replace",path:"is-unapproved",value:!1}],n=t.map(function(e){return{id:e.id,is_unapproved:!1}}),r=t.map(function(e){return{id:e.id,is_unapproved:e.is_unapproved}});c(e,a,n,r)}function l(e){var t=e.selection,a=[{op:"replace",path:"is-protected",value:!0}],n=t.map(function(e){return{id:e.id,is_protected:!0}}),r=t.map(function(e){return{id:e.id,is_protected:e.is_protected}});c(e,a,n,r)}function s(e){var t=e.selection,a=[{op:"replace",path:"is-protected",value:!1}],n=t.map(function(e){return{id:e.id,is_protected:!1}}),r=t.map(function(e){return{id:e.id,is_protected:e.is_protected}});c(e,a,n,r)}function i(e){var t=e.selection,a=[{op:"replace",path:"is-hidden",value:!0}],n=t.map(function(t){return{id:t.id,is_hidden:!0,hidden_on:(0,m["default"])(),hidden_by_name:e.user.username,url:Object.assign(t.url,{hidden_by:e.user.url})}}),r=t.map(function(e){return{id:e.id,is_hidden:e.is_hidden,hidden_on:e.hidden_on,hidden_by_name:e.hidden_by_name,url:e.url}});c(e,a,n,r)}function u(e){var t=e.selection,a=[{op:"replace",path:"is-hidden",value:!1}],n=t.map(function(t){return{id:t.id,is_hidden:!1,hidden_on:(0,m["default"])(),hidden_by_name:e.user.username,url:Object.assign(t.url,{hidden_by:e.user.url})}}),r=t.map(function(e){return{id:e.id,is_hidden:e.is_hidden,hidden_on:e.hidden_on,hidden_by_name:e.hidden_by_name,url:e.url}});c(e,a,n,r)}function c(e,t,a,n){var r=e.selection,o=e.thread;a.forEach(function(e){g.patch(e,e)}),j["default"].dispatch(y.deselectAll());var l={ops:t,ids:r.map(function(e){return e.id})};w["default"].patch(o.api.posts.index,l).then(function(e){e.forEach(function(e){j["default"].dispatch(g.patch(e,e))})},function(e){if(400!==e.status)return n.forEach(function(e){j["default"].dispatch(g.patch(e,e))}),x["default"].apiError(e);var t=[],a=[];e.forEach(function(e){e.detail?(t.push(e),a.push(e.id)):j["default"].dispatch(g.patch(e,e)),n.forEach(function(e){a.indexOf(e)!==-1&&j["default"].dispatch(g.patch(e,e))})});var o={};r.forEach(function(e){o[e.id]=e}),k["default"].show(b["default"].createElement(S["default"],{errors:t,posts:o}))})}function d(e){var t=confirm(gettext("Are you sure you want to merge selected posts? This action is not reversible!"));t&&(e.selection.slice(1).map(function(e){j["default"].dispatch(g.patch(e,{isDeleted:!0}))}),w["default"].post(e.thread.api.posts.merge,{posts:e.selection.map(function(e){return e.id})}).then(function(e){j["default"].dispatch(g.patch(e,g.hydrate(e)))},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),e.selection.slice(1).map(function(e){j["default"].dispatch(g.patch(e,{isDeleted:!1}))})}),j["default"].dispatch(y.deselectAll()))}function f(e){var t=confirm(gettext("Are you sure you want to delete selected posts? This action is not reversible!"));if(t){e.selection.map(function(e){j["default"].dispatch(g.patch(e,{isDeleted:!0}))});var a=e.selection.map(function(e){return e.id});w["default"]["delete"](e.thread.api.posts.index,a).then(function(){},function(t){400===t.status?x["default"].error(t.detail):x["default"].apiError(t),
+e.selection.map(function(e){j["default"].dispatch(g.patch(e,{isDeleted:!1}))})}),j["default"].dispatch(y.deselectAll())}}Object.defineProperty(a,"__esModule",{value:!0}),a.approve=o,a.protect=l,a.unprotect=s,a.hide=i,a.unhide=u,a.patch=c,a.merge=d,a.remove=f;var p=e("moment"),m=r(p),h=e("react"),b=r(h),v=e("../../../../reducers/post"),g=n(v),_=e("../../../../reducers/posts"),y=n(_),E=e("../../../../services/ajax"),w=r(E),O=e("../../../../services/modal"),k=r(O),N=e("../../../../services/snackbar"),x=r(N),P=e("../../../../services/store"),j=r(P),C=e("./errors-list"),S=r(C)},{"../../../../reducers/post":352,"../../../../reducers/posts":353,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"./errors-list":221,moment:"moment",react:"react"}],220:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Delete=a.Unhide=a.Hide=a.Unprotect=a.Protect=a.Split=a.Move=a.Merge=a.Approve=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return c["default"].createElement("ul",{className:"dropdown-menu"},c["default"].createElement(_,e),c["default"].createElement(y,e),c["default"].createElement(E,e),c["default"].createElement(w,e),c["default"].createElement(O,e),c["default"].createElement(k,e),c["default"].createElement(x,e),c["default"].createElement(N,e),c["default"].createElement(P,e))};var u=e("react"),c=r(u),d=e("../../../../services/modal"),f=r(d),p=e("./actions"),m=n(p),h=e("./move"),b=r(h),v=e("./split"),g=r(v),_=a.Approve=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.approve(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_approve&&e.is_unapproved});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}}]),t}(c["default"].Component),y=a.Merge=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.merge(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.length>1&&this.props.selection.find(function(e){return e.acl.can_merge});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge"))):null}}]),t}(c["default"].Component),E=a.Move=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(b["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_move});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}}]),t}(c["default"].Component),w=a.Split=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){f["default"].show(c["default"].createElement(g["default"],n.props))},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_move});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"call_split"),gettext("Split"))):null}}]),t}(c["default"].Component),O=a.Protect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.protect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return!e.is_protected&&e.acl.can_protect});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Protect"))):null}}]),t}(c["default"].Component),k=a.Unprotect=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.unprotect(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.is_protected&&e.acl.can_protect});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Unprotect"))):null}}]),t}(c["default"].Component),N=a.Hide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.hide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_hide&&!e.is_hidden});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}}]),t}(c["default"].Component),x=a.Unhide=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.unhide(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_unhide&&e.is_hidden});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}}]),t}(c["default"].Component),P=a.Delete=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){m.remove(n.props)},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){var e=this.props.selection.find(function(e){return e.acl.can_delete});return e?c["default"].createElement("li",null,c["default"].createElement("button",{type:"button",className:"btn btn-link",onClick:this.onClick},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}}]),t}(c["default"].Component)},{"../../../../services/modal":370,"./actions":219,"./move":223,"./split":224,react:"react"}],221:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.errors,a=e.post,n=interpolate(gettext("%(username)s on %(posted_on)s"),{posted_on:a.posted_on.format("LL, LT"),username:a.poster_name},!0);return l["default"].createElement("li",null,l["default"].createElement("h5",null,n,":"),t.map(function(e,t){return l["default"].createElement("p",{key:t},e)}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.errors,a=e.posts;return l["default"].createElement("div",{className:"modal-dialog",role:"document"},l["default"].createElement("div",{className:"modal-content"},l["default"].createElement("div",{className:"modal-header"},l["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},l["default"].createElement("span",{"aria-hidden":"true"},"×")),l["default"].createElement("h4",{className:"modal-title"},gettext("Moderation"))),l["default"].createElement("div",{className:"modal-body"},l["default"].createElement("p",{className:"lead"},gettext("One or more posts could not be changed:")),l["default"].createElement("ul",{className:"list-unstyled list-errored-items"},t.map(function(e){return l["default"].createElement(r,{errors:e.detail,key:e.id,post:a[e.id]})})))))},a.PostErrors=r;var o=e("react"),l=n(o)},{react:"react"}],222:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(e.acl.can_merge_posts&&t.length>1)return!0;var a=!1;return t.forEach(function(e){if(!e.is_event){var t=e.acl.can_approve&&e.is_unapproved||e.acl.can_delete||!e.is_hidden&&e.acl.can_hide||e.acl.can_move||e.acl.can_merge||e.acl.can_protect||e.is_hidden&&e.acl.can_unhide||e.acl.can_unprotect;t&&(a=!0)}}),a}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){if(!e.user.id||!r(e.thread,e.posts.results))return null;var t=e.posts.results.filter(function(e){return e.isSelected});return s["default"].createElement("div",{className:"dropup"},s["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",disabled:!t.length,type:"button"},gettext("Posts options")),s["default"].createElement(u["default"],o({selection:t},e)))},a.isVisible=r;var l=e("react"),s=n(l),i=e("./dropdown"),u=n(i)},{"./dropdown":220,react:"react"}],223:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Move posts")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../button"),p=(r(f),e("../../../form")),m=r(p),h=e("../../../form-group"),b=r(h),v=e("../../../../reducers/post"),g=n(v),_=e("../../../../services/ajax"),y=r(_),E=e("../../../../services/modal"),w=r(E),O=e("../../../../services/snackbar"),k=r(O),N=e("../../../../services/store"),x=r(N),P=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(k["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return y["default"].post(this.props.thread.api.posts.move,{new_thread:this.state.url,posts:this.props.selection.map(function(e){return e.id})})}},{key:"handleSuccess",value:function(e){this.props.selection.forEach(function(e){x["default"].dispatch(g.patch(e,{isDeleted:!0}))}),w["default"].hide(),k["default"].success(gettext("Selected posts were moved to the other thread."))}},{key:"handleError",value:function(e){400===e.status?k["default"].error(e.detail):k["default"].apiError(e)}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(b["default"],{"for":"id_url",label:gettext("Link to thread you want to move posts to")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading},gettext("Move posts"))))))}}]),t}(m["default"]);a["default"]=P},{"../../../../reducers/post":352,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../button":8,"../../../form":55,"../../../form-group":54,react:"react"}],224:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement(k["default"],null))}function u(e){return m["default"].createElement(c,{className:"modal-dialog modal-message"},m["default"].createElement("div",{className:"message-icon"},m["default"].createElement("span",{className:"material-icon"},"info_outline")),m["default"].createElement("div",{className:"message-body"},m["default"].createElement("p",{className:"lead"},gettext("You can't move selected posts at the moment.")),m["default"].createElement("p",null,e.message),m["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}function c(e){return m["default"].createElement("div",{className:e.className,role:"document"},m["default"].createElement("div",{className:"modal-content"},m["default"].createElement("div",{className:"modal-header"},m["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},m["default"].createElement("span",{"aria-hidden":"true"},"×")),m["default"].createElement("h4",{className:"modal-title"},gettext("Split posts into new thread"))),e.children))}Object.defineProperty(a,"__esModule",{value:!0}),a.ModerationForm=a.PostingConfig=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),f=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return m["default"].createElement(B,f({},e,{Form:H}))},a.Loader=i,a.Error=u,a.Modal=c;var p=e("react"),m=r(p),h=e("../../../button"),b=r(h),v=e("../../../form"),g=r(v),_=e("../../../form-group"),y=r(_),E=e("../../../category-select"),w=r(E),O=e("../../../modal-loader"),k=r(O),N=e("../../../select"),x=r(N),P=e("../../../../reducers/post"),j=n(P),C=e("../../../../services/ajax"),S=r(C),M=e("../../../../services/modal"),T=r(M),L=e("../../../../services/snackbar"),A=r(L),R=e("../../../../services/store"),I=r(R),D=e("../../../../utils/validators"),U=n(D),B=a.PostingConfig=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.state={isLoaded:!1,isError:!1,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;S["default"].get(misago.get("THREAD_EDITOR_API")).then(function(t){var a=t.map(function(e){return Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id,post:e.post})});e.setState({isLoaded:!0,categories:a})},function(t){e.setState({isError:t.detail})})}},{key:"render",value:function(){return this.state.isError?m["default"].createElement(u,{message:this.state.isError}):this.state.isLoaded?m["default"].createElement(H,f({},this.props,{categories:this.state.categories})):m["default"].createElement(i,null)}}]),t}(m["default"].Component),H=a.ModerationForm=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,categories:e.categories,weight:0,is_hidden:0,is_closed:!1,validators:{title:[U.required()]},errors:{}},a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a.acl={},a.props.categories.forEach(function(e){e.post&&(a.state.category||(a.state.category=e.id),a.acl[e.id]={can_pin_threads:e.post.pin,can_close_threads:e.post.close,can_hide_threads:e.post.hide})}),a}return s(t,e),d(t,[{key:"clean",value:function(){return!!this.isValid()||(A["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return S["default"].post(this.props.thread.api.posts.split,{title:this.state.title,category:this.state.category,weight:this.state.weight,is_hidden:this.state.is_hidden,is_closed:this.state.is_closed,posts:this.props.selection.map(function(e){return e.id})})}},{key:"handleSuccess",value:function(e){this.props.selection.forEach(function(e){I["default"].dispatch(j.patch(e,{isDeleted:!0}))}),T["default"].hide(),A["default"].success(gettext("Selected posts were split into new thread."))}},{key:"handleError",value:function(e){400===e.status?(this.setState({errors:Object.assign({},this.state.errors,e)}),A["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?T["default"].show(m["default"].createElement(ErrorsModal,{errors:e})):A["default"].apiError(e)}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?m["default"].createElement(y["default"],{label:gettext("Thread weight"),"for":"id_weight",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?m["default"].createElement(y["default"],{label:gettext("Hide thread"),"for":"id_is_hidden",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?m["default"].createElement(y["default"],{label:gettext("Close thread"),"for":"id_is_closed",labelClass:"col-sm-4",controlClass:"col-sm-8"},m["default"].createElement(x["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"render",value:function(){return m["default"].createElement(c,{className:"modal-dialog"},m["default"].createElement("form",{onSubmit:this.handleSubmit},m["default"].createElement("div",{className:"modal-body"},m["default"].createElement(y["default"],{label:gettext("Thread title"),"for":"id_title",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.title},m["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),m["default"].createElement("div",{className:"clearfix"}),m["default"].createElement(y["default"],{label:gettext("Category"),"for":"id_category",labelClass:"col-sm-4",controlClass:"col-sm-8",validation:this.state.errors.category},m["default"].createElement(w["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.state.categories})),m["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),m["default"].createElement("div",{className:"modal-footer"},m["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),m["default"].createElement(b["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Split posts")))))}}]),t}(g["default"])},{"../../../../reducers/post":352,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../../utils/validators":392,"../../../button":8,"../../../category-select":21,"../../../form":55,"../../../form-group":54,"../../../modal-loader":60,"../../../select":209,react:"react"}],225:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./merge"),f=r(d),p=e("./move"),m=r(p),h=e("../../../../reducers/thread"),b=n(h),v=e("../../../../services/ajax"),g=r(v),_=e("../../../../services/modal"),y=r(_),E=e("../../../../services/snackbar"),w=r(E),O=e("../../../../services/store"),k=r(O),N=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.callApi=function(e,t){k["default"].dispatch(b.busy()),e.push({op:"add",path:"acl",value:!0}),g["default"].patch(n.props.thread.api.index,e).then(function(e){k["default"].dispatch(b.update(e)),k["default"].dispatch(b.release()),w["default"].success(t)},function(e){k["default"].dispatch(b.release()),400===e.status?w["default"].error(e.detail[0]):w["default"].apiError(e)})},n.pinGlobally=function(){n.callApi([{op:"replace",path:"weight",value:2}],gettext("Thread has been pinned globally."))},n.pinLocally=function(){n.callApi([{op:"replace",path:"weight",value:1}],gettext("Thread has been pinned locally."))},n.unpin=function(){n.callApi([{op:"replace",path:"weight",value:0}],gettext("Thread has been unpinned."))},n.approve=function(){n.callApi([{op:"replace",path:"is-unapproved",value:!1}],gettext("Thread has been approved."))},n.open=function(){n.callApi([{op:"replace",path:"is-closed",value:!1}],gettext("Thread has been opened."))},n.close=function(){n.callApi([{op:"replace",path:"is-closed",value:!0}],gettext("Thread has been closed."))},n.unhide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!1}],gettext("Thread has been made visible."))},n.hide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!0}],gettext("Thread has been made hidden."))},n.move=function(){y["default"].show(c["default"].createElement(m["default"],{posts:n.props.posts,thread:n.props.thread}))},n.merge=function(){y["default"].show(c["default"].createElement(f["default"],{thread:n.props.thread}))},n["delete"]=function(){confirm(gettext("Are you sure you want to delete this thread?"))&&(k["default"].dispatch(b.busy()),g["default"]["delete"](n.props.thread.api.index).then(function(e){w["default"].success(gettext("Thread has been deleted.")),window.location=n.props.thread.category.url.index},function(e){k["default"].dispatch(b.release()),w["default"].apiError(e)}))},r=a,l(n,r)}return s(t,e),i(t,[{key:"getPinGloballyButton",value:function(){return 2===this.props.thread.weight?null:this.props.thread.acl.can_pin_globally?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinGlobally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark"),gettext("Pin globally"))):null}},{key:"getPinLocallyButton",value:function(){return 1===this.props.thread.weight?null:this.props.thread.acl.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinLocally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark_border"),gettext("Pin locally"))):null}},{key:"getUnpinButton",value:function(){return 0===this.props.thread.weight?null:this.props.thread.acl.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unpin,type:"button"},c["default"].createElement("span",{className:"material-icon"},"panorama_fish_eye"),gettext("Unpin"))):null}},{key:"getMoveButton",value:function(){return this.props.thread.acl.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.move,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move"))):null}},{key:"getMergeButton",value:function(){return this.props.thread.acl.can_merge?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.merge,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge"))):null}},{key:"getApproveButton",value:function(){return this.props.thread.is_unapproved&&this.props.thread.acl.can_approve?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.approve,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve"))):null}},{key:"getOpenButton",value:function(){return this.props.thread.is_closed&&this.props.thread.acl.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.open,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Open"))):null}},{key:"getCloseButton",value:function(){return this.props.thread.is_closed?null:this.props.thread.acl.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.close,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Close"))):null}},{key:"getUnhideButton",value:function(){return this.props.thread.is_hidden&&this.props.thread.acl.can_unhide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unhide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide"))):null}},{key:"getHideButton",value:function(){return this.props.thread.is_hidden?null:this.props.thread.acl.can_hide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.hide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide"))):null}},{key:"getDeleteButton",value:function(){return this.props.thread.acl.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this["delete"],type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete"))):null}},{key:"render",value:function(){return c["default"].createElement("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},this.getPinGloballyButton(),this.getPinLocallyButton(),this.getUnpinButton(),this.getMoveButton(),this.getMergeButton(),this.getApproveButton(),this.getOpenButton(),this.getCloseButton(),this.getUnhideButton(),this.getHideButton(),this.getDeleteButton());
+}}]),t}(c["default"].Component);a["default"]=N},{"../../../../reducers/thread":359,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"./merge":228,"./move":229,react:"react"}],226:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.isModerationVisible=a.ModerationControls=void 0;var r=e("./controls"),o=n(r),l=e("./is-visible"),s=n(l);a.ModerationControls=o["default"],a.isModerationVisible=s["default"]},{"./controls":225,"./is-visible":227}],227:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return!!(e.acl.can_approve&&e.is_unapproved||e.acl.can_close||e.acl.can_delete||e.acl.can_hide||e.acl.can_move||e.acl.can_merge||e.acl.can_pin||e.acl.can_pin_globally&&2!==e.weight||e.acl.can_unhide&&e.is_hidden)}},{}],228:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return d["default"].createElement("div",{className:"modal-header"},d["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},d["default"].createElement("span",{"aria-hidden":"true"},"×")),d["default"].createElement("h4",{className:"modal-title"},gettext("Merge thread")))}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i;var c=e("react"),d=r(c),f=e("../../../form"),p=r(f),m=e("../../../form-group"),h=r(m),b=e("../../../merge-conflict"),v=r(b),g=e("../../../../reducers/thread"),_=n(g),y=e("../../../../services/ajax"),E=r(y),w=e("../../../../services/modal"),O=r(w),k=e("../../../../services/snackbar"),N=r(k),x=e("../../../../services/store"),P=r(x),j=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.handleSuccess=function(e){a.handleSuccessUnmounted(e),a.setState({isLoading:!0})},a.handleSuccessUnmounted=function(e){N["default"].success(gettext("Thread has been merged with other one.")),window.location=e.url},a.handleError=function(e){P["default"].dispatch(_.release()),400===e.status?e.best_answers||e.polls?O["default"].show(d["default"].createElement(v["default"],{api:a.props.thread.api.merge,bestAnswers:e.best_answers,data:{other_thread:a.state.url},polls:e.polls,onError:a.handleError,onSuccess:a.handleSuccessUnmounted})):e.best_answer?N["default"].error(e.best_answer[0]):e.poll?N["default"].error(e.poll[0]):N["default"].error(e.detail):N["default"].apiError(e)},a.onUrlChange=function(e){a.changeValue("url",e.target.value)},a.state={isLoading:!1,url:"",validators:{url:[]},errors:{}},a}return s(t,e),u(t,[{key:"clean",value:function(){return!!this.state.url.trim().length||(N["default"].error(gettext("You have to enter link to the other thread.")),!1)}},{key:"send",value:function(){return P["default"].dispatch(_.busy()),E["default"].post(this.props.thread.api.merge,{other_thread:this.state.url})}},{key:"render",value:function(){return d["default"].createElement("div",{className:"modal-dialog",role:"document"},d["default"].createElement("form",{onSubmit:this.handleSubmit},d["default"].createElement("div",{className:"modal-content"},d["default"].createElement(i,null),d["default"].createElement("div",{className:"modal-body"},d["default"].createElement(h["default"],{"for":"id_url",label:gettext("Link to thread you want to merge with"),help_text:gettext("Merge will delete current thread and move its contents to the thread specified here.")},d["default"].createElement("input",{className:"form-control",disabled:this.state.isLoading||this.props.thread.isBusy,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),d["default"].createElement("div",{className:"modal-footer"},d["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),d["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading||this.props.thread.isBusy},gettext("Merge thread"))))))}}]),t}(p["default"]);a["default"]=j},{"../../../../reducers/thread":359,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../form":55,"../../../form-group":54,"../../../merge-conflict":58,react:"react"}],229:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return p["default"].createElement("div",{className:"modal-header"},p["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},p["default"].createElement("span",{"aria-hidden":"true"},"×")),p["default"].createElement("h4",{className:"modal-title"},gettext("Move thread")))}function u(e){return p["default"].createElement("div",{className:"modal-dialog",role:"document"},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement(E["default"],null)))}function c(e){return p["default"].createElement("div",{className:"modal-dialog modal-message",role:"document"},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement("div",{className:"message-icon"},p["default"].createElement("span",{className:"material-icon"},"info_outline")),p["default"].createElement("div",{className:"message-body"},p["default"].createElement("p",{className:"lead"},gettext("You can't move this thread at the moment.")),p["default"].createElement("p",null,e.message),p["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok")))))}Object.defineProperty(a,"__esModule",{value:!0});var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ModalHeader=i,a.ModalLoading=u,a.ModalMessage=c;var f=e("react"),p=r(f),m=e("../../../form"),h=r(m),b=e("../../../form-group"),v=r(b),g=e("../../../category-select"),_=r(g),y=e("../../../modal-loader"),E=r(y),w=e("../../../../reducers/posts"),O=n(w),k=e("../../../../reducers/thread"),N=n(k),x=e("../../../.."),P=r(x),j=e("../../../../services/ajax"),C=r(j),S=e("../../../../services/modal"),M=r(S),T=e("../../../../services/snackbar"),L=r(T),A=e("../../../../services/store"),R=r(A),I=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.onCategoryChange=function(e){a.changeValue("category",e.target.value)},a.state={isReady:!1,isLoading:!1,isError:!1,category:null,categories:[]},a}return s(t,e),d(t,[{key:"componentDidMount",value:function(){var e=this;C["default"].get(P["default"].get("THREAD_EDITOR_API")).then(function(t){var a=null,n=t.map(function(e){return e.post===!1||a||(a=e.id),Object.assign(e,{disabled:e.post===!1,label:e.name,value:e.id})});e.setState({isReady:!0,category:a,categories:n})},function(t){e.setState({isError:t.detail})})}},{key:"send",value:function(){return R["default"].dispatch(N.busy()),C["default"].patch(this.props.thread.api.index,[{op:"replace",path:"category",value:this.state.category}])}},{key:"handleSuccess",value:function(){C["default"].get(this.props.thread.api.posts.index,{page:this.props.posts.page}).then(function(e){R["default"].dispatch(N.replace(e)),R["default"].dispatch(O.load(e.post_set)),R["default"].dispatch(N.release()),L["default"].success(gettext("Thread has been moved.")),M["default"].hide()},function(e){R["default"].dispatch(N.release()),L["default"].apiError(e)})}},{key:"handleError",value:function(e){400===e.status?L["default"].error(e.detail[0]):L["default"].apiError(e)}},{key:"render",value:function(){return this.state.isReady?p["default"].createElement("div",{className:"modal-dialog",role:"document"},p["default"].createElement("form",{onSubmit:this.handleSubmit},p["default"].createElement("div",{className:"modal-content"},p["default"].createElement(i,null),p["default"].createElement("div",{className:"modal-body"},p["default"].createElement(v["default"],{"for":"id_category",label:gettext("New category")},p["default"].createElement(_["default"],{choices:this.state.categories,disabled:this.state.isLoading||this.props.thread.isBusy,id:"id_category",onChange:this.onCategoryChange,value:this.state.category}))),p["default"].createElement("div",{className:"modal-footer"},p["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),p["default"].createElement("button",{className:"btn btn-primary",loading:this.state.isLoading||this.props.thread.isBusy},gettext("Move thread")))))):this.state.isError?p["default"].createElement(c,{message:this.state.isError}):p["default"].createElement(u,null)}}]),t}(h["default"]);a["default"]=I},{"../../../..":301,"../../../../reducers/posts":353,"../../../../reducers/thread":359,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../category-select":21,"../../../form":55,"../../../form-group":54,"../../../modal-loader":60,react:"react"}],230:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return d["default"].createElement("div",{className:"row row-paginator"},d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(o,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(l,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(s,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(i,e)))}function o(e){return e.posts.isLoaded&&e.posts.first?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index,title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page"))}function l(e){if(e.posts.isLoaded&&e.posts.page>1){var t="";return e.posts.previous&&(t=e.posts.previous+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+t,title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function s(e){if(e.posts.isLoaded&&e.posts.more){var t="";return e.posts.next&&(t=e.posts.next+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+t,title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function i(e){return e.posts.isLoaded&&e.posts.last?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-outline btn-icon",to:e.thread.url.index+e.posts.last+"/",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-outline btn-icon disabled",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page"))}function u(e){var t=null;return e.more?(t=ngettext("There is %(more)s more post in this thread.","There are %(more)s more posts in this thread.",e.more),t=interpolate(t,{more:e.more},!0)):t=gettext("There are no more posts in this thread."),d["default"].createElement("p",null,t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return d["default"].createElement("nav",{className:"misago-pagination pull-left"},d["default"].createElement(r,e),d["default"].createElement(u,{more:e.posts.more}))},a.Pager=r,a.FirstPage=o,a.PreviousPage=l,a.NextPage=s,a.LastPage=i,a.More=u;var c=e("react"),d=n(c),f=e("react-router")},{react:"react","react-router":"react-router"}],231:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("button",{className:e.className||"btn btn-primary btn-outline",onClick:e.onClick,type:"button"},o["default"].createElement("span",{className:"material-icon"},"chat"),gettext("Reply"))};var r=e("react"),o=n(r)},{react:"react"}],232:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{participants:e.participants,poll:e.poll,posts:e.posts,thread:e.thread,tick:e.tick.tick,user:e.auth.user}}function o(){var e=c["default"].get("THREAD"),t=e.url.index.replace(e.slug+"-"+e.pk,":slug");return[{path:t,component:(0,l.connect)(r)(i["default"])},{path:t+":page/",component:(0,l.connect)(r)(i["default"])}]}Object.defineProperty(a,"__esModule",{value:!0}),a.select=r,a.paths=o;var l=e("react-redux"),s=e("./route"),i=n(s),u=e("../../index"),c=n(u)},{"../../index":301,"./route":233,"react-redux":"react-redux"}],233:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),c=e("react"),d=r(c),f=e("../participants"),p=r(f),m=e("../poll"),h=e("../posts-list"),b=r(h),v=e("./header"),g=r(v),_=e("./toolbar-top"),y=r(_),E=e("./toolbar-bottom"),w=r(E),O=e("../../reducers/participants"),k=n(O),N=e("../../reducers/poll"),x=n(N),P=e("../../reducers/posts"),j=n(P),C=e("../../reducers/thread"),S=n(C),M=e("../../services/ajax"),T=r(M),L=e("../../services/polls"),A=r(L),R=e("../../services/snackbar"),I=r(R),D=e("../../services/posting"),U=r(D),B=e("../../services/store"),H=r(B),z=e("../../services/page-title"),F=r(z),q=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.update=function(e){H["default"].dispatch(S.replace(e)),H["default"].dispatch(j.load(e.post_set)),e.participants&&H["default"].dispatch(k.replace(e.participants)),e.poll&&H["default"].dispatch(x.replace(e.poll)),n.setPageTitle()},n.openReplyForm=function(){U["default"].open({mode:"REPLY",config:n.props.thread.api.editor,submit:n.props.thread.api.posts.index})},r=a,l(n,r)}return s(t,e),u(t,[{key:"componentDidMount",value:function(){this.shouldFetchData()&&(this.fetchData(),this.setPageTitle()),this.startPollingApi()}},{key:"componentDidUpdate",value:function(){this.shouldFetchData()&&(this.fetchData(),this.startPollingApi(),this.setPageTitle())}},{key:"componentWillUnmount",value:function(){this.stopPollingApi()}},{key:"shouldFetchData",value:function(){if(this.props.posts.isLoaded){var e=1*(this.props.params.page||1);return e!=this.props.posts.page}return!1}},{key:"fetchData",value:function(){var e=this;H["default"].dispatch(j.unload()),T["default"].get(this.props.thread.api.posts.index,{page:this.props.params.page||1},"posts").then(function(t){e.update(t)},function(e){I["default"].apiError(e)})}},{key:"startPollingApi",value:function(){A["default"].start({poll:"thread-posts",url:this.props.thread.api.posts.index,data:{page:this.props.params.page||1},update:this.update,frequency:12e4,delayed:!0})}},{key:"stopPollingApi",value:function(){A["default"].stop("thread-posts")}},{key:"setPageTitle",value:function(){F["default"].set({title:this.props.thread.title,parent:this.props.thread.category.name,page:1*(this.props.params.page||1)})}},{key:"render",value:function(){var e="page page-thread";return this.props.thread.category.css_class&&(e+=" page-thread-"+this.props.thread.category.css_class),d["default"].createElement("div",{className:e},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement(g["default"],this.props)),d["default"].createElement("div",{className:"container"},d["default"].createElement(y["default"],i({openReplyForm:this.openReplyForm},this.props)),d["default"].createElement(m.Poll,{poll:this.props.poll,thread:this.props.thread,user:this.props.user}),d["default"].createElement(p["default"],{participants:this.props.participants,thread:this.props.thread,user:this.props.user}),d["default"].createElement(b["default"],this.props),d["default"].createElement(w["default"],i({openReplyForm:this.openReplyForm},this.props))))}}]),t}(d["default"].Component);a["default"]=q},{"../../reducers/participants":350,"../../reducers/poll":351,"../../reducers/posts":353,"../../reducers/thread":359,"../../services/ajax":364,"../../services/page-title":372,"../../services/polls":373,"../../services/posting":374,"../../services/snackbar":375,"../../services/store":376,"../participants":100,"../poll":105,"../posts-list":150,"./header":217,"./toolbar-bottom":235,"./toolbar-top":236,react:"react"}],234:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){return e===!0?"star":e===!1?"star_half":"star_border"}function u(e){return e===!0?gettext("E-mail"):e===!1?gettext("Enabled"):gettext("Disabled")}function c(e){return m["default"].createElement("ul",{className:e.dropdownClassName||"dropdown-menu stick-to-bottom"},m["default"].createElement(O,e),m["default"].createElement(k,e),m["default"].createElement(N,e))}function d(e,t,a){var n={subscription:e.subscription};w["default"].dispatch(b.update({subscription:t})),g["default"].patch(e.api.index,[{op:"replace",path:"subscription",value:a}]).then(function(e){w["default"].dispatch(b.update(e))},function(e){400===e.status?y["default"].error(e.detail[0]):y["default"].apiError(e),w["default"].dispatch(b.update(n))})}Object.defineProperty(a,"__esModule",{value:!0}),a.Email=a.Enable=a.Disable=void 0;var f=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a["default"]=function(e){return e.user.id?m["default"].createElement("div",{className:e.className},m["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",type:"button"},m["default"].createElement("span",{className:"material-icon"},i(e.thread.subscription)),u(e.thread.subscription)),m["default"].createElement(c,e)):null},a.getIcon=i,a.getLabel=u,a.Dropdown=c,a.update=d;var p=e("react"),m=r(p),h=e("../../reducers/thread"),b=n(h),v=e("../../services/ajax"),g=r(v),_=e("../../services/snackbar"),y=r(_),E=e("../../services/store"),w=r(E),O=a.Disable=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){null!==n.props.thread.subscription&&d(n.props.thread,null,"unsubscribe")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star_border"),gettext("Unsubscribe")))}}]),t}(m["default"].Component),k=a.Enable=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.thread.subscription!==!1&&d(n.props.thread,!1,"notify")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star_half"),gettext("Subscribe")))}}]),t}(m["default"].Component),N=a.Email=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){n.props.thread.subscription!==!0&&d(n.props.thread,!0,"email")},r=a,l(n,r)}return s(t,e),f(t,[{key:"render",value:function(){return m["default"].createElement("li",null,m["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick},m["default"].createElement("span",{className:"material-icon"},"star"),gettext("Subscribe with e-mail")))}}]),t}(m["default"].Component)},{"../../reducers/thread":359,"../../services/ajax":364,"../../services/snackbar":375,"../../services/store":376,react:"react"}],235:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.visible?d["default"].createElement("div",{className:"col-md-5"},e.children):null}function o(e){return e.user.id?d["default"].createElement("div",{className:"col-sm-4 hidden-xs"},d["default"].createElement(m["default"],e)):null}function l(e){var t="col-xs-6";return e.thread.acl.can_reply||(t="col-xs-12"),d["default"].createElement("div",{className:t+" col-sm-4"},d["default"].createElement(g["default"],u({btnClassName:"btn-block",className:"dropup"},e)))}function s(e){return e.thread.acl.can_reply?d["default"].createElement("div",{className:"col-xs-6 col-sm-4"},d["default"].createElement(b["default"],{className:"btn btn-primary btn-block btn-outline",onClick:e.onClick})):null}function i(e){return e.thread.acl.can_reply?null:d["default"].createElement("div",{className:"hidden-xs hidden-sm col-sm-4"})}Object.defineProperty(a,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){return d["default"].createElement("div",{className:"row row-toolbar"},d["default"].createElement("div",{className:"col-xs-12 text-center visible-xs-block"},d["default"].createElement(f.More,{more:e.posts.more}),d["default"].createElement("div",{className:"toolbar-vertical-spacer"})),d["default"].createElement("div",{className:"col-md-7"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-4 col-md-5"},d["default"].createElement(f.Pager,e)),d["default"].createElement("div",{className:"col-sm-8 col-md-7 hidden-xs"},d["default"].createElement(f.More,{more:e.posts.more})))),d["default"].createElement(r,{visible:!!e.user.id},d["default"].createElement("div",{className:"toolbar-vertical-spacer hidden-md hidden-lg"}),d["default"].createElement("div",{className:"row"},d["default"].createElement(i,e),d["default"].createElement(o,e),d["default"].createElement(l,e),d["default"].createElement(s,{thread:e.thread,onClick:e.openReplyForm}))))},a.Options=r,a.Moderation=o,a.Subscription=l,a.Reply=s,a.Spacer=i;var c=e("react"),d=n(c),f=e("./paginator"),p=e("./moderation/posts"),m=n(p),h=e("./reply-button"),b=n(h),v=e("./subscription"),g=n(v)},{"./moderation/posts":222,"./paginator":230,"./reply-button":231,"./subscription":234,react:"react"}],236:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.user,a="col-xs-3 col-sm-3 col-md-5";return t.is_anonymous&&(a="col-xs-12 col-sm-3 col-md-5"),w["default"].createElement("div",{className:a},w["default"].createElement("div",{className:"row hidden-xs hidden-sm"},w["default"].createElement(d,{thread:e.thread}),w["default"].createElement(i,{thread:e.thread}),w["default"].createElement(u,{thread:e.thread}),w["default"].createElement(c,{thread:e.thread})),w["default"].createElement(f,e))}function i(e){return e.thread.is_new?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.new_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to first new post")},gettext("New"))):null}function u(e){return e.thread.best_answer?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.best_answer,className:"btn btn-default btn-block btn-outline",title:gettext("Go to best answer")},gettext("Best answer"))):null}function c(e){return e.thread.has_unapproved_posts&&e.thread.acl.can_approve?w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.unapproved_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to first unapproved post")},gettext("Unapproved"))):null}function d(e){return w["default"].createElement("div",{className:"col-sm-4"},w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-default btn-block btn-outline",title:gettext("Go to last post")},gettext("Last")))}function f(e){var t=e.user;return t.is_anonymous?w["default"].createElement("div",{className:"visible-xs-block visible-sm-block"},w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-default btn-block btn-outline"},gettext("Last post"))):w["default"].createElement("div",{className:"dropdown visible-xs-block visible-sm-block"},w["default"].createElement("button",{"aria-expanded":"true","aria-haspopup":"true",className:"btn btn-default dropdown-toggle btn-block btn-outline","data-toggle":"dropdown",type:"button"},w["default"].createElement("span",{className:"material-icon"},"expand_more"),w["default"].createElement("span",{className:"btn-text hidden-xs"},gettext("Options"))),w["default"].createElement("ul",{className:"dropdown-menu"},w["default"].createElement(S,e),w["default"].createElement(p,e),w["default"].createElement(m,e),w["default"].createElement(h,e)))}function p(e){return e.thread.is_new?w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.new_post,className:"btn btn-link"},gettext("Go to first new post"))):null}function m(e){return e.thread.has_unapproved_posts&&e.thread.acl.can_approve?w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.unapproved_post,className:"btn btn-link"},gettext("Go to first unapproved post"))):null}function h(e){return w["default"].createElement("li",null,w["default"].createElement("a",{href:e.thread.url.last_post,className:"btn btn-link"},gettext("Go to last post")))}function b(e){return e.thread.acl.can_reply?w["default"].createElement("div",{className:"col-sm-4 hidden-xs"},w["default"].createElement(k["default"],{className:"btn btn-primary btn-block btn-outline",onClick:e.openReplyForm})):null}function v(e){return e.user.id?w["default"].createElement("div",{className:"col-xs-12 col-sm-4"},w["default"].createElement(x["default"],y({className:"dropdown",dropdownClassName:"dropdown-menu dropdown-menu-right stick-to-bottom"},e))):null}function g(e){return e.visible?w["default"].createElement("div",{className:"col-sm-4 hidden-xs"}):null}Object.defineProperty(a,"__esModule",{value:!0}),a.StartPollCompact=a.StartPoll=void 0;var _=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t};
+}(),y=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e};a["default"]=function(e){var t=!e.thread.acl.can_start_poll||e.thread.poll;return w["default"].createElement("div",{className:"row row-toolbar row-toolbar-bottom-margin"},w["default"].createElement(s,e),w["default"].createElement("div",{className:"col-xs-9 col-md-5 col-md-offset-2"},w["default"].createElement("div",{className:"row"},w["default"].createElement(g,{visible:!e.user.id}),w["default"].createElement(g,{visible:t}),w["default"].createElement(v,e),w["default"].createElement(C,e),w["default"].createElement(b,e))))},a.GotoMenu=s,a.GotoNew=i,a.GotoBestAnswer=u,a.GotoUnapproved=c,a.GotoLast=d,a.CompactOptions=f,a.GotoNewCompact=p,a.GotoUnapprovedCompact=m,a.GotoLastCompact=h,a.Reply=b,a.SubscriptionMenu=v,a.Spacer=g;var E=e("react"),w=n(E),O=e("./reply-button"),k=n(O),N=e("./subscription"),x=n(N),P=e("../../services/posting"),j=n(P),C=a.StartPoll=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.onClick=function(){j["default"].open({mode:"POLL",submit:n.props.thread.api.poll,thread:n.props.thread,poll:null})},l=a,o(n,l)}return l(t,e),_(t,[{key:"render",value:function(){return!this.props.thread.acl.can_start_poll||this.props.thread.poll?null:w["default"].createElement("div",{className:"col-sm-4 hidden-xs"},w["default"].createElement("button",{className:"btn btn-default btn-block btn-outline",onClick:this.onClick,type:"button"},w["default"].createElement("span",{className:"material-icon"},"poll"),gettext("Add poll")))}}]),t}(w["default"].Component),S=a.StartPollCompact=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),_(t,[{key:"render",value:function(){return!this.props.thread.acl.can_start_poll||this.props.thread.poll?null:w["default"].createElement("li",null,w["default"].createElement("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},gettext("Add poll")))}}]),t}(C)},{"../../services/posting":374,"./reply-button":231,"./subscription":234,react:"react"}],237:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.isLoaded?0===e.threads.length?o["default"].createElement(s["default"],{diffSize:e.diffSize,applyDiff:e.applyDiff},e.children):o["default"].createElement(u["default"],{activeCategory:e.category,categories:e.categories,list:e.list,threads:e.threads,diffSize:e.diffSize,applyDiff:e.applyDiff,showOptions:e.showOptions,selection:e.selection,busyThreads:e.busyThreads}):o["default"].createElement(d["default"],null)};var r=e("react"),o=n(r),l=e("./list/empty"),s=n(l),i=e("./list/ready"),u=n(i),c=e("./list/preview"),d=n(c)},{"./list/empty":239,"./list/preview":240,"./list/ready":241,react:"react"}],238:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=ngettext("There is %(threads)s new or updated thread. Click this message to show it.","There are %(threads)s new or updated threads. Click this message to show them.",e);return interpolate(t,{threads:e},!0)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.diffSize,a=e.applyDiff;return 0===t?null:l["default"].createElement("li",{className:"list-group-item threads-diff-message"},l["default"].createElement("button",{type:"button",className:"btn btn-block btn-default",onClick:a},l["default"].createElement("span",{className:"material-icon"},"cached"),l["default"].createElement("span",{className:"diff-message"},r(t))))},a.getMessage=r;var o=e("react"),l=n(o)},{react:"react"}],239:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./diff-message"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getDiffMessage",value:function(){return 0===this.props.diffSize?null:u["default"].createElement(d["default"],{applyDiff:this.props.applyDiff,diffSize:this.props.diffSize})}},{key:"render",value:function(){return u["default"].createElement("div",{className:"threads-list ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.getDiffMessage(),this.props.children))}}]),t}(u["default"].Component);a["default"]=f},{"./diff-message":238,react:"react"}],240:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../thread/preview"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",{className:"threads-list ui-preview"},u["default"].createElement("ul",{className:"list-group"},u["default"].createElement(d["default"],null)))}}]),t}(u["default"].Component);a["default"]=f},{"../thread/preview":248,react:"react"}],241:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",{className:"threads-list ui-ready"},o["default"].createElement("ul",{className:"list-group"},o["default"].createElement(s["default"],{diffSize:e.diffSize,applyDiff:e.applyDiff}),e.threads.map(function(t){return o["default"].createElement(u["default"],{activeCategory:e.activeCategory,categories:e.categories,list:e.list,thread:t,showOptions:e.showOptions,isSelected:e.selection.indexOf(t.id)>=0,isBusy:e.busyThreads.indexOf(t.id)>=0,key:t.id})})))};var r=e("react"),o=n(r),l=e("./diff-message"),s=n(l),i=e("../thread/ready"),u=n(i)},{"../thread/ready":249,"./diff-message":238,react:"react"}],242:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-hidden"},f["default"].createElement("span",{className:"material-icon"},"visibility_off"),f["default"].createElement("span",{className:a},gettext("Hidden"))):null}function o(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-closed"},f["default"].createElement("span",{className:"material-icon"},"lock_outline"),f["default"].createElement("span",{className:a},gettext("Closed"))):null}function l(e){var t=e.display,a=e.textClassName;return t?f["default"].createElement("span",{className:"thread-detail-poll"},f["default"].createElement("span",{className:"material-icon"},"assessment"),f["default"].createElement("span",{className:a},gettext("Poll"))):null}function s(e){var t=e.thread;return t.best_answer?f["default"].createElement("a",{className:"visible-xs-inline-block thread-detail-answered",href:t.url.best_answer},f["default"].createElement("span",{className:"material-icon"},"check_box")):null}function i(e){var t=e.replies,a=e.forceFullText,n=ngettext("%(replies)s reply","%(replies)s replies",t),r="",o="";return a?(r="detail-text hide",o="detail-text"):(r="detail-text visible-xs-inline-block",o="detail-text hidden-xs"),f["default"].createElement("span",{className:"thread-detail-replies"},f["default"].createElement("span",{className:"material-icon"},"forum"),f["default"].createElement("span",{className:r},t),f["default"].createElement("span",{className:o},interpolate(n,{replies:t},!0)))}function u(e){var t=e.datetime,a=e.url;return f["default"].createElement("a",{className:"visible-sm-inline-block thread-detail-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}function c(e){var t=e.posterName,a=e.url,n="visible-sm-inline-block item-title thread-last-poster";return a?f["default"].createElement("a",{className:n,href:a},t):f["default"].createElement("span",{className:n},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.isBusy,n=e.showOptions,d=e.isSelected,p=e.thread,b="col-xs-12 col-sm-12";n&&(b=p.moderation.length?"col-xs-6 col-sm-12":"col-xs-9 col-sm-12");var v=0;p.is_hidden&&(v+=1),p.is_closed&&(v+=1),p.has_poll&&(v+=1);var g=n&&3===v,_="detail-text hidden-xs";return g&&(_+=" hidden-sm"),f["default"].createElement("div",{className:"row thread-details-bottom"},f["default"].createElement("div",{className:b},f["default"].createElement(m["default"],{className:"item-title thread-detail-category hidden-xs",category:t}),f["default"].createElement(r,{textClassName:_,display:p.is_hidden}),f["default"].createElement(o,{textClassName:_,display:p.is_closed}),f["default"].createElement(l,{textClassName:_,display:p.has_poll}),f["default"].createElement(s,{thread:p}),f["default"].createElement(i,{forceFullText:!n||v<2,replies:p.replies}),f["default"].createElement(u,{datetime:p.last_post_on,url:p.url.last_post}),f["default"].createElement(c,{posterName:p.last_poster_name,url:p.url.last_poster})),f["default"].createElement(h.OptionsXs,{disabled:a,display:n,isSelected:d,thread:p}))},a.HiddenLabel=r,a.ClosedLabel=o,a.PollLabel=l,a.BestAnswerLabel=s,a.RepliesLabel=i,a.LastReplyLabel=u,a.LastPoster=c;var d=e("react"),f=n(d),p=e("./category"),m=n(p),h=e("../options")},{"../options":247,"./category":243,react:"react"}],243:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.className;return t?(t.css_class&&(a+=" thread-detail-category-"+t.css_class),o["default"].createElement("a",{className:a,href:t.url.index},t.name)):null};var r=e("react"),o=n(r)},{react:"react"}],244:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a.TopDetails=a.BottomDetails=void 0;var r=e("./bottom"),o=n(r),l=e("./top"),s=n(l);a.BottomDetails=o["default"],a.TopDetails=s["default"]},{"./bottom":242,"./top":245}],245:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.isRead,a=e.url;return t?null:d["default"].createElement("a",{className:"thread-detail-new",href:a},d["default"].createElement("span",{className:"material-icon"},"comment"),d["default"].createElement("span",{className:"detail-text"},gettext("New posts")))}function o(e){var t=e.weight;if(0===t)return null;var a="thread-detail-pinned-globally",n="bookmark",r=gettext("Pinned globally");return 1===t&&(a="thread-detail-pinned-locally",n="bookmark_border",r=gettext("Pinned locally")),d["default"].createElement("span",{className:a},d["default"].createElement("span",{className:"material-icon"},n),d["default"].createElement("span",{className:"detail-text"},r))}function l(e){var t=e.posts,a=e.thread;if(!t&&!a)return null;var n="thread-detail-unapproved-posts",r="remove_circle_outline",o=gettext("Unapproved posts");return a&&(n="thread-detail-unapproved",r="remove_circle",o=gettext("Unapproved")),d["default"].createElement("span",{className:n},d["default"].createElement("span",{className:"material-icon"},r),d["default"].createElement("span",{className:"detail-text"},o))}function s(e){var t=e.thread;return t.best_answer?d["default"].createElement("a",{className:"hidden-xs thread-detail-answered",href:t.url.best_answer},d["default"].createElement("span",{className:"material-icon"},"check_box"),d["default"].createElement("span",{className:"detail-text"},gettext("Answered"))):null}function i(e){var t=e.datetime,a=e.url;return d["default"].createElement("a",{className:"visible-xs-inline-block thread-detail-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}function u(e){var t=e.posterName,a=e.url;return a?d["default"].createElement("a",{className:"visible-xs-inline-block item-title thread-last-poster",href:a},t):d["default"].createElement("span",{className:"visible-xs-inline-block item-title thread-last-poster"},t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.category,a=e.thread;return d["default"].createElement("div",{className:"thread-details-top"},d["default"].createElement(r,{isRead:a.is_read,url:a.url.new_post}),d["default"].createElement(o,{weight:a.weight}),d["default"].createElement(l,{thread:a.is_unapproved,posts:a.has_unapproved_posts}),d["default"].createElement(s,{thread:a}),d["default"].createElement(p["default"],{className:"item-title thread-detail-category visible-xs-inline-block",category:t}),d["default"].createElement(i,{datetime:a.last_post_on,url:a.url.last_post}),d["default"].createElement(u,{posterName:a.last_poster_name,url:a.url.last_poster}))},a.NewLabel=r,a.PinnedLabel=o,a.UnapprovedLabel=l,a.BestAnswerLabel=s,a.LastReplyLabel=i,a.LastPoster=u;var c=e("react"),d=n(c),f=e("./category"),p=n(f)},{"./category":243,react:"react"}],246:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.datetime,a=e.url;return l["default"].createElement("a",{className:"thread-last-reply",href:a,title:t.format("LLL")},t.fromNow(!0))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.thread;return l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left"},l["default"].createElement(c["default"],{className:"thread-last-poster-avatar",title:t.last_poster_name,url:t.url.last_poster},l["default"].createElement(i["default"],{className:"media-object",size:40,user:t.last_poster}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement(c["default"],{className:"item-title thread-last-poster",url:t.url.last_poster},t.last_poster_name),l["default"].createElement(r,{datetime:t.last_post_on,url:t.url.last_post})))},a.Timestamp=r;var o=e("react"),l=n(o),s=e("../../avatar"),i=n(s),u=e("./user-url"),c=n(u)},{"../../avatar":6,"./user-url":254,react:"react"}],247:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function i(e){var t=e.display,a=e.disabled,n=e.isSelected,r=e.thread;if(!t)return null;var o="col-sm-2 col-md-2 hidden-xs";return r.moderation.length&&(o="col-sm-3 col-md-2 hidden-xs"),f["default"].createElement("div",{className:o},f["default"].createElement("div",{className:"row thread-options"},f["default"].createElement(b["default"],{thread:r,disabled:a}),f["default"].createElement(m["default"],{thread:r,disabled:a}),f["default"].createElement(E,{thread:r,disabled:a,isSelected:n})))}function u(e){var t=e.display,a=e.disabled,n=e.isSelected,r=e.thread;if(!t)return null;var o="";return o+=r.moderation.length?"col-xs-6":"col-xs-3",o+=" visible-xs-block thread-options-xs",f["default"].createElement("div",{className:o},f["default"].createElement("div",{className:"row thread-options"},f["default"].createElement(b["default"],{thread:r,disabled:a}),f["default"].createElement(m["default"],{thread:r,disabled:a}),f["default"].createElement(E,{thread:r,disabled:a,isSelected:n})))}Object.defineProperty(a,"__esModule",{value:!0}),a.Checkbox=void 0;var c=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Options=i,a.OptionsXs=u;var d=e("react"),f=r(d),p=e("./subscription/compact"),m=r(p),h=e("./subscription/full"),b=r(h),v=e("../../../reducers/selection"),g=n(v),_=e("../../../services/store"),y=r(_),E=a.Checkbox=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.toggleSelection=function(){y["default"].dispatch(g.item(n.props.thread.id))},r=a,l(n,r)}return s(t,e),c(t,[{key:"render",value:function(){var e=this.props,t=e.disabled,a=e.isSelected,n=e.thread;return n.moderation.length?f["default"].createElement("div",{className:"col-xs-6"},f["default"].createElement("button",{className:"btn btn-default btn-icon btn-block",onClick:this.toggleSelection,disabled:t},f["default"].createElement("span",{className:"material-icon"},a?"check_box":"check_box_outline_blank"))):null}}]),t}(f["default"].Component)},{"../../../reducers/selection":357,"../../../services/store":376,"./subscription/compact":250,"./subscription/full":251,react:"react"}],248:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../../utils/random"),f=n(d),p=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("li",{className:"list-group-item thread-preview"},c["default"].createElement("div",{className:"thread-details-top visible-xs-block"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," ")),c["default"].createElement("span",{className:"item-title thread-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](60,200)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text hidden-xs",style:{width:f["int"](60,200)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text hidden-xs",style:{width:f["int"](60,200)+"px"}}," ")),c["default"].createElement("div",{className:"thread-details-bottom"},c["default"].createElement("div",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "),c["default"].createElement("span",{className:"ui-preview-text",style:{width:f["int"](30,80)+"px"}}," "))))}}]),t}(c["default"].Component);a["default"]=p},{"../../../utils/random":387,react:"react"}],249:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a,n){var r=["list-group-item"];return n&&n.css_class&&(r.push("list-group-category-has-flavor"),r.push("list-group-item-category-"+n.css_class)),e?r.push("thread-read"):r.push("thread-new"),t?r.push("thread-busy"):a&&r.push("thread-selected"),r.join(" ")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.activeCategory,a=e.categories,n=(e.list,e.thread),o=e.isBusy,s=e.isSelected,c=e.showOptions,p=null;t.id!==n.category&&(p=a[n.category]);var h=p||t,b="thread-main col-xs-12";return b+=c?n.moderation.length?" col-sm-9 col-md-7":" col-sm-10 col-md-7":" col-sm-12 col-md-9",l["default"].createElement("li",{className:r(n.is_read,o,s,h)},l["default"].createElement(u.TopDetails,{category:p,thread:n}),l["default"].createElement("div",{className:"row thread-row"},l["default"].createElement("div",{className:b},l["default"].createElement("div",{className:"media"},l["default"].createElement("div",{className:"media-left hidden-xs"},l["default"].createElement(m["default"],{className:"thread-starter-avatar",title:n.starter_name,url:n.url.starter},l["default"].createElement(i["default"],{size:40,user:n.starter}))),l["default"].createElement("div",{className:"media-body"},l["default"].createElement("a",{href:n.url.index,className:"item-title thread-title"},n.title),l["default"].createElement(u.BottomDetails,{category:p,disabled:o,isSelected:s,showOptions:c,thread:n})))),l["default"].createElement("div",{className:"col-md-3 hidden-xs hidden-sm thread-last-action"},l["default"].createElement(d["default"],{thread:n})),l["default"].createElement(f.Options,{disabled:o,display:c,isSelected:s,thread:n})))},a.getClassName=r;var o=e("react"),l=n(o),s=e("../../avatar"),i=n(s),u=e("./details"),c=e("./last-action"),d=n(c),f=e("./options"),p=e("./user-url"),m=n(p)},{"../../avatar":6,"./details":244,"./last-action":246,"./options":247,"./user-url":254,react:"react"}],250:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./full"),d=n(c),f=e("./modal"),p=n(f),m=e("../../../../services/modal"),h=n(m),b=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),c=0;c<s;c++)i[c]=arguments[c];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.showOptions=function(){h["default"].show(u["default"].createElement(p["default"],{thread:n.props.thread}))},l=a,o(n,l)}return l(t,e),s(t,[{key:"render",value:function(){var e=this.props.thread.moderation,t="";return t+=e.length?"col-xs-6":"col-xs-12",t+=" hidden-md hidden-lg",u["default"].createElement("div",{className:t},u["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,onClick:this.showOptions},u["default"].createElement("span",{className:"material-icon"},this.getIcon())))}}]),t}(d["default"]);a["default"]=b},{"../../../../services/modal":370,"./full":251,"./modal":252,react:"react"}],251:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.moderation,a=e.subscription;if(t.length)return null;var n=gettext("Disabled");return a===!0?n=gettext("E-mail"):a===!1&&(n=gettext("Enabled")),c["default"].createElement("span",{className:"btn-text"},n)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.Label=s;var u=e("react"),c=n(u),d=e("./options"),f=n(d),p=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"getIcon",value:function(){return this.props.thread.subscription===!0?"star":this.props.thread.subscription===!1?"star_half":"star_border"}},{key:"getClassName",value:function(){return this.props.thread.subscription===!0?"btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-full dropdown-toggle":this.props.thread.subscription===!1?"btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-half dropdown-toggle":"btn btn-default btn-icon btn-block btn-subscribe dropdown-toggle"}},{key:"render",value:function(){var e=this.props.thread,t=e.moderation,a=e.subscription,n=!t.length,r=n?"col-xs-12":"col-xs-6";return r+=" hidden-xs hidden-sm",c["default"].createElement("div",{className:r},c["default"].createElement("div",{className:"btn-group btn-group-justified"},c["default"].createElement("div",{className:"btn-group"},c["default"].createElement("button",{type:"button",className:this.getClassName(),disabled:this.props.disabled,"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},c["default"].createElement("span",{className:"material-icon"},this.getIcon()),c["default"].createElement(s,{moderation:t,subscription:a})),c["default"].createElement(f["default"],{className:"dropdown-menu dropdown-menu-right",thread:this.props.thread}))))}}]),t}(c["default"].Component);a["default"]=p},{"./options":253,react:"react"}],252:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./options"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"modal-dialog modal-sm",role:"document"},u["default"].createElement("div",{className:"modal-content"},u["default"].createElement("div",{className:"modal-header"},u["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},u["default"].createElement("span",{"aria-hidden":"true"},"×")),u["default"].createElement("h4",{className:"modal-title"},gettext("Change subscription"))),u["default"].createElement(d["default"],{className:"modal-menu",thread:this.props.thread})))}}]),t}(u["default"].Component);a["default"]=f},{"./options":253,react:"react"}],253:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../../button"),d=(n(c),e("../../../../reducers/threads")),f=e("../../../../services/ajax"),p=n(f),m=e("../../../../services/modal"),h=n(m),b=e("../../../../services/snackbar"),v=n(b),g=e("../../../../services/store"),_=n(g),y={unsubscribe:null,notify:!1,email:!0},E=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.setSubscription=function(e){h["default"].hide(),a.setState({isLoading:!0});var t=a.props.thread.subscription;_["default"].dispatch((0,d.patch)(a.props.thread,{subscription:y[e]})),p["default"].patch(a.props.thread.api.index,[{op:"replace",path:"subscription",value:e}]).then(function(){
+a.setState({isLoading:!1})},function(e){a.setState({isLoading:!1}),_["default"].dispatch((0,d.patch)(a.props.thread,{subscription:y[t]})),v["default"].apiError(e)})},a.unsubscribe=function(){a.setSubscription("unsubscribe")},a.notify=function(){a.setSubscription("notify")},a.email=function(){a.setSubscription("email")},a.state={isLoading:!1},a}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("ul",{className:this.props.className},u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.unsubscribe},u["default"].createElement("span",{className:"material-icon"},"star_border"),gettext("Unsubscribe"))),u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.notify},u["default"].createElement("span",{className:"material-icon"},"star_half"),gettext("Subscribe"))),u["default"].createElement("li",null,u["default"].createElement("button",{className:"btn-link",onClick:this.email},u["default"].createElement("span",{className:"material-icon"},"star"),gettext("Subscribe with e-mail"))))}}]),t}(u["default"].Component);a["default"]=E},{"../../../../reducers/threads":360,"../../../../services/ajax":364,"../../../../services/modal":370,"../../../../services/snackbar":375,"../../../../services/store":376,"../../../button":8,react:"react"}],254:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.children,a=e.className,n=e.title,r=e.url;return r?o["default"].createElement("a",{className:a,href:r,title:n},t):o["default"].createElement("span",{className:a,title:n},t)};var r=e("react"),o=n(r)},{react:"react"}],255:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.Subcategory=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("react-router"),d=a.Subcategory=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getUrl",value:function(){return this.props.listPath?this.props.category.url.index+this.props.listPath:this.props.category.url.index}},{key:"render",value:function(){return u["default"].createElement("li",null,u["default"].createElement(c.Link,{to:this.getUrl(),className:"btn btn-link"},this.props.category.name))}}]),t}(u["default"].Component),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){var e=this;return u["default"].createElement("div",{className:"dropdown category-picker"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline dropdown-toggle btn-block","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},u["default"].createElement("span",{className:"material-icon"},"label_outline"),u["default"].createElement("span",{className:"hidden-xs"},gettext("Category"))),u["default"].createElement("ul",{className:"dropdown-menu stick-to-bottom categories-menu"},this.props.choices.map(function(t){return e.props.categories[t]?u["default"].createElement(d,{category:e.props.categories[t],listPath:e.props.list.path,key:t}):null})))}}]),t}(u["default"].Component);a["default"]=f},{react:"react","react-router":"react-router"}],256:[function(e,t,a){"use strict";function n(e,t){return e.last_post>t.last_post?-1:e.last_post<t.last_post?1:0}function r(e,t){return 2===e.weight&&e.weight>t.weight?-1:2===t.weight&&e.weight<t.weight?1:n(e,t)}function o(e,t){return e.weight>t.weight?-1:e.weight<t.weight?1:n(e,t)}Object.defineProperty(a,"__esModule",{value:!0}),a.compareLastPostAge=n,a.compareGlobalWeight=r,a.compareWeight=o},{}],257:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../page-lead"),d=n(c),f=e("./toolbar"),p=n(f),m=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getCategoryDescription",value:function(){return this.props.pageLead?u["default"].createElement("div",{className:"category-description"},u["default"].createElement("div",{className:"page-lead"},u["default"].createElement("p",null,this.props.pageLead))):this.props.route.category.description?u["default"].createElement("div",{className:"category-description"},u["default"].createElement(d["default"],{copy:this.props.route.category.description.html})):null}},{key:"getDisableToolbar",value:function(){return!this.props.isLoaded||this.props.isBusy||this.props.busyThreads.length}},{key:"getToolbar",value:function(){var e=this.props.subcategories.length||this.props.user.id;return e?u["default"].createElement(p["default"],{subcategories:this.props.subcategories,categories:this.props.route.categories,categoriesMap:this.props.route.categoriesMap,list:this.props.route.list,threads:this.props.threads,moderation:this.props.moderation,selection:this.props.selection,selectAllThreads:this.props.selectAllThreads,selectNoneThreads:this.props.selectNoneThreads,addThreads:this.props.addThreads,freezeThread:this.props.freezeThread,deleteThread:this.props.deleteThread,updateThread:this.props.updateThread,api:this.props.api,route:this.props.route,disabled:this.getDisableToolbar(),user:this.props.user}):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"container"},this.getCategoryDescription(),this.getToolbar(),this.props.children)}}]),t}(u["default"].Component);a["default"]=m},{"../page-lead":91,"./toolbar":268,react:"react"}],258:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.categories,a=e.category;if(!a)return null;var n=t[a];return c["default"].createElement(d.Link,{className:"go-back-sm visible-xs-block",to:n.url.index},c["default"].createElement("span",{className:"material-icon"},"chevron_left"),n.parent?n.name:gettext("Threads"))}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ParentCategory=s;var u=e("react"),c=n(u),d=e("react-router"),f=e("../button"),p=n(f),m=e("../dropdown-toggle"),h=(n(m),e("./nav")),b=n(h),v=e("../../services/ajax"),g=(n(v),e("../../services/posting")),_=n(g),y=e("../../services/snackbar"),E=(n(y),e("../../services/store")),w=(n(E),e("../..")),O=n(w),k=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.startThread=function(){_["default"].open(a.props.startThread||{mode:"START",config:O["default"].get("THREAD_EDITOR_API"),submit:O["default"].get("THREADS_API"),category:a.props.route.category.id})},a.state={isBusy:!1},a}return l(t,e),i(t,[{key:"hasGoBackButton",value:function(){return!!this.props.route.category.parent}},{key:"getGoBackButton",value:function(){if(!this.props.route.category.parent)return null;var e=this.props.categories[this.props.route.category.parent];return c["default"].createElement("div",{className:"hidden-xs col-sm-2 col-lg-1"},c["default"].createElement(d.Link,{className:"btn btn-default btn-icon btn-aligned btn-go-back btn-block btn-outline",to:e.url.index+this.props.route.list.path},c["default"].createElement("span",{className:"material-icon"},"keyboard_arrow_left")))}},{key:"getStartThreadButton",value:function(){return this.props.user.id?c["default"].createElement(p["default"],{className:"btn-primary btn-block btn-outline",onClick:this.startThread,disabled:this.props.disabled},c["default"].createElement("span",{className:"material-icon"},"chat"),gettext("Start thread")):null}},{key:"render",value:function(){var e="col-xs-12";this.hasGoBackButton()&&(e+=" col-sm-10 col-lg-11 sm-align-row-buttons");var t=!!this.props.user.id;return c["default"].createElement("div",{className:"page-header-bg"},c["default"].createElement("div",{className:"page-header"},c["default"].createElement("div",{className:"container"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:t?"col-sm-9 col-md-10":"col-xs-12"},c["default"].createElement("div",{className:"row"},this.getGoBackButton(),c["default"].createElement("div",{className:e},c["default"].createElement(s,{categories:this.props.categories,category:this.props.route.category.parent}),c["default"].createElement("h1",null,this.props.title)))),t&&c["default"].createElement("div",{className:"col-sm-3 col-md-2 xs-margin-top"},this.getStartThreadButton()))),c["default"].createElement(b["default"],{baseUrl:this.props.route.category.url.index,list:this.props.route.list,lists:this.props.route.lists})))}}]),t}(c["default"].Component);a["default"]=k},{"../..":301,"../../services/ajax":364,"../../services/posting":374,"../../services/snackbar":375,"../../services/store":376,"../button":8,"../dropdown-toggle":27,"./nav":265,react:"react","react-router":"react-router"}],259:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return"all"===this.props.list.type?this.props.emptyMessage?u["default"].createElement("li",{className:"list-group-item empty-message"},u["default"].createElement("p",{className:"lead"},this.props.emptyMessage),u["default"].createElement("p",null,gettext("Why not start one yourself?"))):u["default"].createElement("li",{className:"list-group-item empty-message"},u["default"].createElement("p",{className:"lead"},this.props.category.special_role?gettext("There are no threads on this forum... yet!"):gettext("There are no threads in this category.")),u["default"].createElement("p",null,gettext("Why not start one yourself?"))):u["default"].createElement("li",{className:"list-group-item empty-message"},gettext("No threads matching specified criteria were found."))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],260:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./errors-list"),f=r(d),p=e("./merge"),m=r(p),h=e("./move"),b=r(h),v=e("../../../reducers/selection"),g=(n(v),e("../../../services/ajax")),_=r(g),y=e("../../../services/modal"),E=r(y),w=e("../../../services/snackbar"),O=r(w),k=e("../../../services/store"),N=(r(k),e("../../../utils/countdown")),x=(r(N),function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.callApi=function(e,t){var a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;n.props.threads.forEach(function(e){n.props.freezeThread(e.id)});var r=n.props.threads.map(function(e){return e.id});e.push({op:"add",path:"acl",value:!0}),_["default"].patch(n.props.api,{ids:r,ops:e}).then(function(e){n.props.threads.forEach(function(e){n.props.freezeThread(e.id)}),e.forEach(function(e){n.props.updateThread(e)}),O["default"].success(t),a&&a()},function(e){if(n.props.threads.forEach(function(e){n.props.freezeThread(e.id)}),400!==e.status)return O["default"].apiError(e);var t=[],a={};n.props.threads.forEach(function(e){a[e.id]=e}),e.forEach(function(e){var n=e.id,r=e.detail;"undefined"!=typeof a[n]&&t.push({errors:r,thread:a[n]})}),E["default"].show(c["default"].createElement(f["default"],{errors:t}))})},n.pinGlobally=function(){n.callApi([{op:"replace",path:"weight",value:2}],gettext("Selected threads were pinned globally."))},n.pinLocally=function(){n.callApi([{op:"replace",path:"weight",value:1}],gettext("Selected threads were pinned locally."))},n.unpin=function(){n.callApi([{op:"replace",path:"weight",value:0}],gettext("Selected threads were unpinned."))},n.approve=function(){n.callApi([{op:"replace",path:"is-unapproved",value:!1}],gettext("Selected threads were approved."))},n.open=function(){n.callApi([{op:"replace",path:"is-closed",value:!1}],gettext("Selected threads were opened."))},n.close=function(){n.callApi([{op:"replace",path:"is-closed",value:!0}],gettext("Selected threads were closed."))},n.unhide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!1}],gettext("Selected threads were unhidden."))},n.hide=function(){n.callApi([{op:"replace",path:"is-hidden",value:!0}],gettext("Selected threads were hidden."))},n.move=function(){E["default"].show(c["default"].createElement(b["default"],{callApi:n.callApi,categories:n.props.categories,categoriesMap:n.props.categoriesMap,route:n.props.route,user:n.props.user}))},n.merge=function(){var e=[];if(n.props.threads.forEach(function(t){t.acl.can_merge||e.append({id:t.id,title:t.title,errors:[gettext("You don't have permission to merge this thread with others.")]})}),n.props.threads.length<2)O["default"].info(gettext("You have to select at least two threads to merge."));else{if(e.length)return void E["default"].show(c["default"].createElement(f["default"],{errors:e}));E["default"].show(c["default"].createElement(m["default"],n.props))}},n["delete"]=function(){if(confirm(gettext("Are you sure you want to delete selected threads?"))){n.props.threads.map(function(e){n.props.freezeThread(e.id)});var e=n.props.threads.map(function(e){return e.id});_["default"]["delete"](n.props.api,e).then(function(){n.props.threads.map(function(e){n.props.freezeThread(e.id),n.props.deleteThread(e)}),O["default"].success(gettext("Selected threads were deleted."))},function(e){if(400===e.status){var t=e.map(function(e){return e.id});n.props.threads.map(function(e){n.props.freezeThread(e.id),t.indexOf(e.id)===-1&&n.props.deleteThread(e)}),E["default"].show(c["default"].createElement(f["default"],{errors:e}))}else O["default"].apiError(e)})}},r=a,l(n,r)}return s(t,e),i(t,[{key:"getPinGloballyButton",value:function(){return this.props.moderation.can_pin_globally?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinGlobally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark"),gettext("Pin threads globally"))):null}},{key:"getPinLocallyButton",value:function(){return this.props.moderation.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.pinLocally,type:"button"},c["default"].createElement("span",{className:"material-icon"},"bookmark_border"),gettext("Pin threads locally"))):null}},{key:"getUnpinButton",value:function(){return this.props.moderation.can_pin?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unpin,type:"button"},c["default"].createElement("span",{className:"material-icon"},"panorama_fish_eye"),gettext("Unpin threads"))):null}},{key:"getMoveButton",value:function(){return this.props.moderation.can_move?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.move,type:"button"},c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),gettext("Move threads"))):null}},{key:"getMergeButton",value:function(){return this.props.moderation.can_merge?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.merge,type:"button"},c["default"].createElement("span",{className:"material-icon"},"call_merge"),gettext("Merge threads"))):null}},{key:"getApproveButton",value:function(){return this.props.moderation.can_approve?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.approve,type:"button"},c["default"].createElement("span",{className:"material-icon"},"done"),gettext("Approve threads"))):null}},{key:"getOpenButton",value:function(){return this.props.moderation.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.open,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_open"),gettext("Open threads"))):null}},{key:"getCloseButton",value:function(){return this.props.moderation.can_close?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.close,type:"button"},c["default"].createElement("span",{className:"material-icon"},"lock_outline"),gettext("Close threads"))):null}},{key:"getUnhideButton",value:function(){return this.props.moderation.can_unhide?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this.unhide,type:"button"},c["default"].createElement("span",{className:"material-icon"},"visibility"),gettext("Unhide threads"))):null}},{key:"getHideButton",value:function(){return this.props.moderation.can_hide?c["default"].createElement("li",null,c["default"].createElement("button",{onClick:this.hide,type:"button",className:"btn btn-link"},c["default"].createElement("span",{className:"material-icon"},"visibility_off"),gettext("Hide threads"))):null}},{key:"getDeleteButton",value:function(){return this.props.moderation.can_delete?c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",onClick:this["delete"],type:"button"},c["default"].createElement("span",{className:"material-icon"},"clear"),gettext("Delete threads"))):null}},{key:"render",value:function(){return c["default"].createElement("ul",{className:this.props.className},this.getPinGloballyButton(),this.getPinLocallyButton(),this.getUnpinButton(),this.getMoveButton(),this.getMergeButton(),this.getApproveButton(),this.getOpenButton(),this.getCloseButton(),this.getUnhideButton(),this.getHideButton(),this.getDeleteButton())}}]),t}(c["default"].Component));a["default"]=x},{"../../../reducers/selection":357,"../../../services/ajax":364,"../../../services/modal":370,"../../../services/snackbar":375,"../../../services/store":376,"../../../utils/countdown":381,"./errors-list":261,"./merge":262,"./move":263,react:"react"}],261:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.errors,a=e.thread;return c["default"].createElement("li",null,c["default"].createElement("h5",null,a.title),t.map(function(e,t){return c["default"].createElement("p",null,e)}))}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.ThreadErrors=s;var u=e("react"),c=n(u),d=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("div",{className:"modal-dialog",role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{"aria-label":gettext("Close"),className:"close","data-dismiss":"modal",type:"button"},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Threads moderation"))),c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("p",{className:"lead"},gettext("One or more threads could not be deleted:")),c["default"].createElement("ul",{className:"list-unstyled list-errored-items"},this.props.errors.map(function(e){return c["default"].createElement(s,{errors:e.errors,key:e.thread.id,thread:e.thread})})))))}}]),t}(c["default"].Component);a["default"]=d},{react:"react"}],262:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../button"),f=r(d),p=e("../../form"),m=r(p),h=e("../../form-group"),b=r(h),v=e("../../category-select"),g=r(v),_=e("../../select"),y=r(_),E=e("../../../index"),w=r(E),O=e("../../../reducers/threads"),k=e("../../../reducers/selection"),N=n(k),x=e("./errors-list"),P=r(x),j=e("../../merge-conflict"),C=r(j),S=e("../../../services/ajax"),M=r(S),T=e("../../../services/modal"),L=r(T),A=e("../../../services/snackbar"),R=r(A),I=e("../../../services/store"),D=r(I),U=e("../../../utils/validators"),B=n(U),H=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.getFormdata=function(){return{threads:a.props.threads.map(function(e){return e.id}),title:a.state.title,category:a.state.category,weight:a.state.weight,is_hidden:a.state.is_hidden,is_closed:a.state.is_closed}},a.handleSuccess=function(e){a.props.threads.forEach(function(e){a.props.freezeThread(e.id),a.props.deleteThread(e)}),D["default"].dispatch(N.none()),a.props.addThreads([e]),D["default"].dispatch((0,O.filterThreads)(a.props.route.category,a.props.categoriesMap)),L["default"].hide()},a.handleError=function(e){400===e.status?e.best_answers||e.polls?L["default"].show(c["default"].createElement(C["default"],{api:w["default"].get("MERGE_THREADS_API"),bestAnswers:e.best_answers,data:a.getFormdata(),polls:e.polls,onError:a.handleError,onSuccess:a.handleSuccess})):(a.setState({errors:Object.assign({},a.state.errors,e)}),R["default"].error(gettext("Form contains errors."))):403===e.status&&Array.isArray(e)?L["default"].show(c["default"].createElement(P["default"],{errors:e})):e.best_answer?R["default"].error(e.best_answer[0]):e.poll?R["default"].error(e.poll[0]):R["default"].apiError(e)},a.onCategoryChange=function(e){var t=e.target.value,n={category:t};a.acl[t].can_pin_threads<n.weight&&(n.weight=0),a.acl[t].can_hide_threads||(n.is_hidden=0),a.acl[t].can_close_threads||(n.is_closed=!1),a.setState(n)},a.state={isLoading:!1,title:"",category:null,weight:0,is_hidden:0,is_closed:!1,validators:{title:[B.required()]},errors:{}},a.acl={};for(var n in e.user.acl.categories)if(e.user.acl.categories.hasOwnProperty(n)){var r=e.user.acl.categories[n];a.acl[r.id]=r}return a.categoryChoices=[],e.categories.forEach(function(e){if(e.level>0){var t=a.acl[e.id],n=!t.can_start_threads||e.is_closed&&!t.can_close_threads;a.categoryChoices.push({value:e.id,disabled:n,level:e.level-1,label:e.name}),n||a.state.category||(a.state.category=e.id)}}),a.isHiddenChoices=[{value:0,icon:"visibility",label:gettext("No")},{value:1,icon:"visibility_off",label:gettext("Yes")}],a.isClosedChoices=[{value:!1,icon:"lock_outline",label:gettext("No")},{value:!0,icon:"lock",label:gettext("Yes")}],a}return s(t,e),i(t,[{key:"clean",value:function(){return!!this.isValid()||(R["default"].error(gettext("Form contains errors.")),this.setState({errors:this.validate()}),!1)}},{key:"send",value:function(){return M["default"].post(w["default"].get("MERGE_THREADS_API"),this.getFormdata())}},{key:"getWeightChoices",value:function(){var e=[{value:0,icon:"remove",label:gettext("Not pinned")},{value:1,icon:"bookmark_border",label:gettext("Pinned locally")}];return 2==this.acl[this.state.category].can_pin_threads&&e.push({value:2,icon:"bookmark",label:gettext("Pinned globally")}),e}},{key:"renderWeightField",value:function(){return this.acl[this.state.category].can_pin_threads?c["default"].createElement(b["default"],{label:gettext("Thread weight"),"for":"id_weight"},c["default"].createElement(y["default"],{id:"id_weight",onChange:this.bindInput("weight"),value:this.state.weight,choices:this.getWeightChoices()})):null}},{key:"renderHiddenField",value:function(){return this.acl[this.state.category].can_hide_threads?c["default"].createElement(b["default"],{label:gettext("Hide thread"),"for":"id_is_hidden"},c["default"].createElement(y["default"],{id:"id_is_closed",onChange:this.bindInput("is_hidden"),value:this.state.is_hidden,choices:this.isHiddenChoices})):null}},{key:"renderClosedField",value:function(){return this.acl[this.state.category].can_close_threads?c["default"].createElement(b["default"],{label:gettext("Close thread"),"for":"id_is_closed"},c["default"].createElement(y["default"],{id:"id_is_closed",onChange:this.bindInput("is_closed"),value:this.state.is_closed,choices:this.isClosedChoices})):null}},{key:"renderForm",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(b["default"],{label:gettext("Thread title"),"for":"id_title",validation:this.state.errors.title},c["default"].createElement("input",{id:"id_title",className:"form-control",type:"text",onChange:this.bindInput("title"),value:this.state.title})),c["default"].createElement("div",{className:"clearfix"}),c["default"].createElement(b["default"],{label:gettext("Category"),"for":"id_category",validation:this.state.errors.category},c["default"].createElement(g["default"],{id:"id_category",onChange:this.onCategoryChange,value:this.state.category,choices:this.categoryChoices})),c["default"].createElement("div",{className:"clearfix"}),this.renderWeightField(),this.renderHiddenField(),this.renderClosedField()),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement(f["default"],{className:"btn-primary",loading:this.state.isLoading},gettext("Merge threads"))))}},{key:"renderCantMergeMessage",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("You can't move threads because there are no categories you are allowed to move them to.")),c["default"].createElement("p",null,gettext("You need permission to start threads in category to be able to merge threads to it.")),c["default"].createElement("button",{
+className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}},{key:"getClassName",value:function(){return this.state.category?"modal-dialog":"modal-dialog modal-message"}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Merge threads"))),this.state.category?this.renderForm():this.renderCantMergeMessage()))}}]),t}(m["default"]);a["default"]=H},{"../../../index":301,"../../../reducers/selection":357,"../../../reducers/threads":360,"../../../services/ajax":364,"../../../services/modal":370,"../../../services/snackbar":375,"../../../services/store":376,"../../../utils/validators":392,"../../button":8,"../../category-select":21,"../../form":55,"../../form-group":54,"../../merge-conflict":58,"../../select":209,"./errors-list":261,react:"react"}],263:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../form"),f=r(d),p=e("../../form-group"),m=r(p),h=e("../../category-select"),b=r(h),v=e("../../../reducers/selection"),g=n(v),_=e("../../../reducers/threads"),y=e("../../../services/modal"),E=r(y),w=e("../../../services/store"),O=r(w),k=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.handleSubmit=function(e){e.preventDefault(),E["default"].hide();var t=function(){O["default"].dispatch((0,_.filterThreads)(a.props.route.category,a.props.categoriesMap));var e=O["default"].getState(),t=e.threads.map(function(e){return e.id});O["default"].dispatch(g.all(e.selection.filter(function(e){return t.indexOf(e)!==-1})))};a.props.callApi([{op:"replace",path:"category",value:a.state.category},{op:"replace",path:"flatten-categories",value:null},{op:"add",path:"acl",value:!0}],gettext("Selected threads were moved."),t)},a.state={category:null};var n={};for(var r in e.user.acl.categories)if(e.user.acl.categories.hasOwnProperty(r)){var s=e.user.acl.categories[r];n[s.id]=s}return a.categoryChoices=[],e.categories.forEach(function(e){if(e.level>0){var t=n[e.id],r=!t.can_start_threads||e.is_closed&&!t.can_close_threads;a.categoryChoices.push({value:e.id,disabled:r,level:e.level-1,label:e.name}),r||a.state.category||(a.state.category=e.id)}}),a}return s(t,e),i(t,[{key:"getClassName",value:function(){return this.state.category?"modal-dialog":"modal-dialog modal-message"}},{key:"renderForm",value:function(){return c["default"].createElement("form",{onSubmit:this.handleSubmit},c["default"].createElement("div",{className:"modal-body"},c["default"].createElement(m["default"],{label:gettext("New category"),"for":"id_new_category"},c["default"].createElement(b["default"],{id:"id_new_category",onChange:this.bindInput("category"),value:this.state.category,choices:this.categoryChoices}))),c["default"].createElement("div",{className:"modal-footer"},c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},gettext("Cancel")),c["default"].createElement("button",{className:"btn btn-primary"},gettext("Move threads"))))}},{key:"renderCantMoveMessage",value:function(){return c["default"].createElement("div",{className:"modal-body"},c["default"].createElement("div",{className:"message-icon"},c["default"].createElement("span",{className:"material-icon"},"info_outline")),c["default"].createElement("div",{className:"message-body"},c["default"].createElement("p",{className:"lead"},gettext("You can't move threads because there are no categories you are allowed to move them to.")),c["default"].createElement("p",null,gettext("You need permission to start threads in category to be able to move threads to it.")),c["default"].createElement("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},gettext("Ok"))))}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName(),role:"document"},c["default"].createElement("div",{className:"modal-content"},c["default"].createElement("div",{className:"modal-header"},c["default"].createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":gettext("Close")},c["default"].createElement("span",{"aria-hidden":"true"},"×")),c["default"].createElement("h4",{className:"modal-title"},gettext("Move threads"))),this.state.category?this.renderForm():this.renderCantMoveMessage()))}}]),t}(f["default"]);a["default"]=k},{"../../../reducers/selection":357,"../../../reducers/threads":360,"../../../services/modal":370,"../../../services/store":376,"../../category-select":21,"../../form":55,"../../form-group":54,react:"react"}],264:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../../reducers/selection"),f=n(d),p=e("../../../services/store"),m=r(p),h=function(e){function t(){var e,a,n,r;o(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=l(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.selectAll=function(){m["default"].dispatch(f.all(n.props.threads.map(function(e){return e.id})))},n.selectNone=function(){m["default"].dispatch(f.none())},r=a,l(n,r)}return s(t,e),i(t,[{key:"render",value:function(){return c["default"].createElement("ul",{className:this.props.className},c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",type:"button",onClick:this.selectAll},c["default"].createElement("span",{className:"material-icon"},"check_box"),gettext("Select all"))),c["default"].createElement("li",null,c["default"].createElement("button",{className:"btn btn-link",type:"button",onClick:this.selectNone},c["default"].createElement("span",{className:"material-icon"},"check_box_outline_blank"),gettext("Select none"))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../reducers/selection":357,"../../../services/store":376,react:"react"}],265:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.baseUrl,a=e.list,n=e.lists;return n.length<2?null:o["default"].createElement("div",{className:"page-tabs"},o["default"].createElement("div",{className:"container"},o["default"].createElement("ul",{className:"nav nav-pills"},n.map(function(e){return o["default"].createElement(i["default"],{isControlled:!0,isActive:e.path===a.path,key:t+e.path},o["default"].createElement(l.Link,{to:t+e.path},e.name))}))))};var r=e("react"),o=n(r),l=e("react-router"),s=e("../li"),i=n(s)},{"../li":56,react:"react","react-router":"react-router"}],266:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return function(t){return{options:e,selection:t.selection,threads:t.threads,tick:t.tick.tick,user:t.auth.user}}}function o(e){var t=[{type:"all",path:"",name:gettext("All"),longName:gettext("All threads")}];return e.id&&(t.push({type:"my",path:"my/",name:gettext("My"),longName:gettext("My threads")}),t.push({type:"new",path:"new/",name:gettext("New"),longName:gettext("New threads")}),t.push({type:"unread",path:"unread/",name:gettext("Unread"),longName:gettext("Unread threads")}),t.push({type:"subscribed",path:"subscribed/",name:gettext("Subscribed"),longName:gettext("Subscribed threads")}),e.acl.can_see_unapproved_content_lists&&t.push({type:"unapproved",path:"unapproved/",name:gettext("Unapproved"),longName:gettext("Unapproved content")})),t}function l(e,t){var a=o(e),n=[],l={};return d["default"].get("CATEGORIES").forEach(function(e){a.forEach(function(o){l[e.id]=e,n.push({path:e.url.index+o.path,component:(0,s.connect)(r(t))(u["default"]),categories:d["default"].get("CATEGORIES"),categoriesMap:l,category:e,lists:a,list:o})})}),n}Object.defineProperty(a,"__esModule",{value:!0}),a.getSelect=r,a.getLists=o,a.paths=l;var s=e("react-redux"),i=e("./route"),u=n(i),c=e("../../index"),d=n(c)},{"../../index":301,"./route":267,"react-redux":"react-redux"}],267:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../button"),f=r(d),p=e("./compare"),m=e("./container"),h=r(m),b=e("./header"),v=r(b),g=e("./utils"),_=e("../threads-list"),y=r(_),E=e("./list-empty"),w=r(E),O=e("../with-dropdown"),k=r(O),N=e("../../index"),x=r(N),P=e("../../reducers/selection"),j=n(P),C=e("../../reducers/threads"),S=e("../../services/ajax"),M=r(S),T=e("../../services/polls"),L=r(T),A=e("../../services/snackbar"),R=r(A),I=e("../../services/store"),D=r(I),U=e("../../services/page-title"),B=r(U),H=e("../../utils/sets"),z=n(H),F=function(e){function t(e){o(this,t);var a=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));a.loadMore=function(){a.setState({isBusy:!0}),a.loadThreads(a.getCategory(),a.state.page+1)},a.pollResponse=function(e){a.setState({diff:Object.assign({},e,{results:(0,g.diffThreads)(a.props.threads,e.results)})})},a.addThreads=function(e){D["default"].dispatch((0,C.append)(e,a.getSorting()))},a.applyDiff=function(){a.addThreads(a.state.diff.results),a.setState(Object.assign({},a.state.diff,{moderation:(0,g.getModerationActions)(D["default"].getState().threads),diff:{results:[]}}))},a.freezeThread=function(e){a.setState(function(t){return{busyThreads:z.toggle(t.busyThreads,e)}})},a.updateThread=function(e){D["default"].dispatch((0,C.patch)(e,e,a.getSorting()))},a.deleteThread=function(e){D["default"].dispatch((0,C.deleteThread)(e))},a.state={isMounted:!0,isLoaded:!1,isBusy:!1,diff:{results:[]},moderation:[],busyThreads:[],dropdown:!1,subcategories:[],count:0,more:0,page:1,pages:1};var n=a.getCategory();return x["default"].has("THREADS")?a.initWithPreloadedData(n,x["default"].get("THREADS")):a.initWithoutPreloadedData(n),a}return s(t,e),i(t,[{key:"getCategory",value:function(){return this.props.route.category.special_role?null:this.props.route.category.id}},{key:"initWithPreloadedData",value:function(e,t){this.state=Object.assign(this.state,{moderation:(0,g.getModerationActions)(t.results),subcategories:t.subcategories,count:t.count,more:t.more,page:t.page,pages:t.pages}),this.startPolling(e)}},{key:"initWithoutPreloadedData",value:function(e){this.loadThreads(e)}},{key:"loadThreads",value:function(e){var t=this,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;M["default"].get(this.props.options.api,{category:e,list:this.props.route.list.type,page:a||1},"threads").then(function(n){t.state.isMounted&&(1===a?D["default"].dispatch((0,C.hydrate)(n.results)):D["default"].dispatch((0,C.append)(n.results,t.getSorting())),t.setState({isLoaded:!0,isBusy:!1,moderation:(0,g.getModerationActions)(D["default"].getState().threads),subcategories:n.subcategories,count:n.count,more:n.more,page:n.page,pages:n.pages}),t.startPolling(e))},function(e){R["default"].apiError(e)})}},{key:"startPolling",value:function(e){L["default"].start({poll:"threads",url:this.props.options.api,data:{category:e,list:this.props.route.list.type},frequency:12e4,update:this.pollResponse})}},{key:"componentDidMount",value:function(){this.setPageTitle(),x["default"].has("THREADS")&&(D["default"].dispatch((0,C.hydrate)(x["default"].pop("THREADS").results)),this.setState({isLoaded:!0})),D["default"].dispatch(j.none())}},{key:"componentWillUnmount",value:function(){this.state.isMounted=!1,L["default"].stop("threads")}},{key:"getTitle",value:function(){return this.props.options.title?this.props.options.title:(0,g.getTitle)(this.props.route)}},{key:"setPageTitle",value:function(){this.props.route.category.level||!x["default"].get("THREADS_ON_INDEX")?B["default"].set((0,g.getPageTitle)(this.props.route)):this.props.options.title?B["default"].set(this.props.options.title):x["default"].get("SETTINGS").forum_index_title?document.title=x["default"].get("SETTINGS").forum_index_title:document.title=x["default"].get("SETTINGS").forum_name}},{key:"getSorting",value:function(){return this.props.route.category.level?p.compareWeight:p.compareGlobalWeight}},{key:"getMoreButton",value:function(){return this.state.more?c["default"].createElement("div",{className:"pager-more"},c["default"].createElement(f["default"],{className:"btn btn-default btn-outline",loading:this.state.isBusy||this.state.busyThreads.length,onClick:this.loadMore},gettext("Show more"))):null}},{key:"getClassName",value:function(){var e="page page-threads";return e+=" page-threads-"+this.props.route.list.type,this.props.route.category.css_class&&(e+=" page-threads-"+this.props.route.category.css_class),e}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName()},c["default"].createElement(v["default"],{categories:this.props.route.categoriesMap,disabled:!this.state.isLoaded,startThread:this.props.options.startThread,threads:this.props.threads,title:this.getTitle(),toggleNav:this.toggleNav,route:this.props.route,user:this.props.user}),c["default"].createElement(h["default"],{api:this.props.options.api,route:this.props.route,subcategories:this.state.subcategories,user:this.props.user,pageLead:this.props.options.pageLead,threads:this.props.threads,threadsCount:this.state.count,moderation:this.state.moderation,selection:this.props.selection,busyThreads:this.state.busyThreads,addThreads:this.addThreads,freezeThread:this.freezeThread,deleteThread:this.deleteThread,updateThread:this.updateThread,isLoaded:this.state.isLoaded,isBusy:this.state.isBusy},c["default"].createElement(y["default"],{category:this.props.route.category,categories:this.props.route.categoriesMap,list:this.props.route.list,selection:this.props.selection,threads:this.props.threads,diffSize:this.state.diff.results.length,applyDiff:this.applyDiff,showOptions:!!this.props.user.id,isLoaded:this.state.isLoaded,busyThreads:this.state.busyThreads},c["default"].createElement(w["default"],{category:this.props.route.category,emptyMessage:this.props.options.emptyMessage,list:this.props.route.list})),this.getMoreButton()))}}]),t}(k["default"]);a["default"]=F},{"../../index":301,"../../reducers/selection":357,"../../reducers/threads":360,"../../services/ajax":364,"../../services/page-title":372,"../../services/polls":373,"../../services/snackbar":375,"../../services/store":376,"../../utils/sets":390,"../button":8,"../threads-list":237,"../with-dropdown":298,"./compare":256,"./container":257,"./header":258,"./list-empty":259,"./utils":269,react:"react"}],268:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./category-picker"),d=n(c),f=e("./moderation/controls"),p=n(f),m=e("./moderation/selection"),h=n(m),b=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getCategoryPicker",value:function(){return this.props.subcategories.length?u["default"].createElement(d["default"],{categories:this.props.categoriesMap,choices:this.props.subcategories,list:this.props.list}):null}},{key:"showModerationOptions",value:function(){return this.props.user.id&&this.props.moderation.allow}},{key:"getSelectedThreads",value:function(){var e=this;return this.props.threads.filter(function(t){return e.props.selection.indexOf(t.id)>=0})}},{key:"getModerationButton",value:function(){return this.showModerationOptions()?u["default"].createElement("div",{className:"col-xs-6 col-sm-3 col-md-2"},u["default"].createElement("div",{className:"btn-group btn-group-justified"},u["default"].createElement("div",{className:"btn-group dropdown"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false",disabled:this.props.disabled||!this.props.selection.length},u["default"].createElement("span",{className:"material-icon"},"settings"),gettext("Options")),u["default"].createElement(p["default"],{addThreads:this.props.addThreads,api:this.props.api,categories:this.props.categories,categoriesMap:this.props.categoriesMap,className:"dropdown-menu dropdown-menu-right stick-to-bottom",deleteThread:this.props.deleteThread,freezeThread:this.props.freezeThread,moderation:this.props.moderation,route:this.props.route,threads:this.getSelectedThreads(),updateThread:this.props.updateThread,user:this.props.user})))):null}},{key:"getSelectionButton",value:function(){return this.showModerationOptions()?u["default"].createElement("div",{className:"col-xs-3 col-sm-2 col-md-1"},u["default"].createElement("div",{className:"btn-group btn-group-justified"},u["default"].createElement("div",{className:"btn-group dropdown"},u["default"].createElement("button",{type:"button",className:"btn btn-default btn-outline btn-icon dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false",disabled:this.props.disabled},u["default"].createElement("span",{className:"material-icon"},"select_all")),u["default"].createElement(h["default"],{className:"dropdown-menu dropdown-menu-right stick-to-bottom",threads:this.props.threads})))):null}},{key:"render",value:function(){return u["default"].createElement("div",{className:"row row-toolbar row-toolbar-bottom-margin"},u["default"].createElement("div",{className:"col-xs-3 col-sm-3 col-md-2 dropdown"},this.getCategoryPicker()),u["default"].createElement("div",{className:"hidden-xs col-sm-4 col-md-7"}),this.getModerationButton(),this.getSelectionButton())}}]),t}(u["default"].Component);a["default"]=b},{"./category-picker":255,"./moderation/controls":260,"./moderation/selection":264,react:"react"}],269:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return e.category.level?e.list.path?{title:e.list.longName,parent:e.category.name}:{title:e.category.name}:c["default"].get("THREADS_ON_INDEX")?e.list.path?{title:e.list.longName}:null:e.list.path?{title:e.list.longName,parent:gettext("Threads")}:{title:gettext("Threads")}}function o(e){return e.category.level?e.category.name:c["default"].get("THREADS_ON_INDEX")?c["default"].get("SETTINGS").forum_index_title?c["default"].get("SETTINGS").forum_index_title:c["default"].get("SETTINGS").forum_name:gettext("Threads")}function l(e,t){return[e.title===t.title,e.weight===t.weight,e.category===t.category,e.last_post===t.last_post,e.last_poster_name===t.last_poster_name].indexOf(!1)>=0}function s(e,t){var a={};return e.forEach(function(e){a[e.id]=e}),t.filter(function(e){return!a[e.id]||l(a[e.id],e)})}function i(e){var t={allow:!1,can_approve:0,can_close:0,can_delete:0,can_hide:0,can_merge:0,can_move:0,can_pin:0,can_pin_globally:0,can_unhide:0};return e.forEach(function(e){e.is_unapproved&&e.acl.can_approve>t.can_approve&&(t.can_approve=e.acl.can_approve),e.acl.can_close>t.can_close&&(t.can_close=e.acl.can_close),e.acl.can_delete>t.can_delete&&(t.can_delete=e.acl.can_delete),e.acl.can_hide>t.can_hide&&(t.can_hide=e.acl.can_hide),e.acl.can_merge>t.can_merge&&(t.can_merge=e.acl.can_merge),e.acl.can_move>t.can_move&&(t.can_move=e.acl.can_move),e.acl.can_pin>t.can_pin&&(t.can_pin=e.acl.can_pin),e.acl.can_pin_globally>t.can_pin_globally&&(t.can_pin_globally=e.acl.can_pin_globally),e.acl.can_unhide>t.can_unhide&&(t.can_unhide=e.acl.can_unhide),t.allow=t.can_approve||t.can_close||t.can_delete||t.can_hide||t.can_merge||t.can_move||t.can_pin||t.can_pin_globally||t.can_unhide}),t}Object.defineProperty(a,"__esModule",{value:!0}),a.getPageTitle=r,a.getTitle=o,a.isThreadChanged=l,a.diffThreads=s,a.getModerationActions=i;var u=e("../../index"),c=n(u)},{"../../index":301}],270:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactGuestNav=a.GuestNav=a.GuestMenu=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=e("../navbar-search"),p=n(f),m=e("../register-button"),h=n(m),b=e("../sign-in.js"),v=n(b),g=e("../../services/mobile-navbar-dropdown"),_=n(g),y=e("../../services/modal"),E=n(y),w=a.GuestMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"showSignInModal",value:function(){E["default"].show(v["default"])}},{key:"render",value:function(){return u["default"].createElement("ul",{className:"dropdown-menu user-dropdown dropdown-menu-right",role:"menu"},u["default"].createElement("li",{className:"guest-preview"},u["default"].createElement("h4",null,gettext("You are browsing as guest.")),u["default"].createElement("p",null,gettext("Sign in or register to start and participate in discussions.")),u["default"].createElement("div",{className:"row"},u["default"].createElement("div",{className:"col-xs-6"},u["default"].createElement("button",{className:"btn btn-default btn-sign-in btn-block",onClick:this.showSignInModal,type:"button"},gettext("Sign in"))),u["default"].createElement("div",{className:"col-xs-6"},u["default"].createElement(h["default"],{className:"btn-primary btn-register btn-block"},gettext("Register"))))))}}]),t}(u["default"].Component);a.GuestNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"nav nav-guest"},u["default"].createElement("button",{className:"btn navbar-btn btn-default btn-sign-in",onClick:this.showSignInModal,type:"button"},gettext("Sign in")),u["default"].createElement(h["default"],{className:"navbar-btn btn-primary btn-register"},gettext("Register")),u["default"].createElement("div",{className:"navbar-left"},u["default"].createElement(p["default"],null)))}}]),t}(w),a.CompactGuestNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"showGuestMenu",value:function(){_["default"].show(w)}},{key:"render",value:function(){return u["default"].createElement("button",{type:"button",onClick:this.showGuestMenu},u["default"].createElement(d["default"],{size:"64"}))}}]),t}(u["default"].Component)},{"../../services/mobile-navbar-dropdown":369,"../../services/modal":370,"../avatar":6,"../navbar-search":76,"../register-button":197,"../sign-in.js":210,react:"react"}],271:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return e.auth}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactUserMenu=a.UserMenu=void 0;var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s;var u=e("react"),c=n(u),d=e("./guest-nav"),f=e("./user-nav");a.UserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return this.props.isAuthenticated?c["default"].createElement(f.UserNav,{user:this.props.user}):c["default"].createElement(d.GuestNav,null)}}]),t}(c["default"].Component),a.CompactUserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),i(t,[{key:"render",value:function(){return this.props.isAuthenticated?c["default"].createElement(f.CompactUserNav,{user:this.props.user}):c["default"].createElement(d.CompactGuestNav,null)}}]),t}(c["default"].Component)},{"./guest-nav":270,"./user-nav":272,react:"react"}],272:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t=e.user;return t.unread_private_threads?p["default"].createElement("span",{className:"badge"},t.unread_private_threads):null}function i(e){var t=e.user;return p["default"].createElement("ul",{className:"ul nav navbar-nav nav-user"},p["default"].createElement("li",null,p["default"].createElement(y["default"],null)),p["default"].createElement(u,{user:t}),p["default"].createElement("li",{className:"dropdown"},p["default"].createElement("a",{"aria-haspopup":"true","aria-expanded":"false",className:"dropdown-toggle","data-toggle":"dropdown",href:t.url,role:"button"},p["default"].createElement(b["default"],{user:t,size:"64"})),p["default"].createElement(P,{user:t})))}function u(e){var t=e.user;if(!t.acl.can_use_private_threads)return null;var a=null;return a=t.unread_private_threads?gettext("You have unread private threads!"):gettext("Private threads"),p["default"].createElement("li",null,p["default"].createElement("a",{className:"navbar-icon",href:w["default"].get("PRIVATE_THREADS_URL"),title:a},p["default"].createElement("span",{className:"material-icon"},"message"),t.unread_private_threads>0&&p["default"].createElement("span",{className:"badge"},t.unread_private_threads)))}function c(e){return{user:e.auth.user}}Object.defineProperty(a,"__esModule",{value:!0}),a.CompactUserNav=a.UserMenu=void 0;var d=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.PrivateThreadsBadge=s,a.UserNav=i,a.UserPrivateThreadsLink=u,a.selectUserMenu=c;var f=e("react"),p=n(f),m=e("react-redux"),h=e("../avatar"),b=n(h),v=e("../change-avatar/root"),g=n(v),_=e("../navbar-search"),y=n(_),E=e("../.."),w=n(E),O=e("../../services/mobile-navbar-dropdown"),k=n(O),N=e("../../services/modal"),x=n(N),P=a.UserMenu=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),d(t,[{key:"logout",value:function(){var e=confirm(gettext("Are you sure you want to sign out?"));
+e&&$("#hidden-logout-form").submit()}},{key:"changeAvatar",value:function(){x["default"].show((0,m.connect)(v.select)(g["default"]))}},{key:"render",value:function(){var e=this.props.user;return p["default"].createElement("ul",{className:"dropdown-menu user-dropdown dropdown-menu-right",role:"menu"},p["default"].createElement("li",{className:"dropdown-header"},p["default"].createElement("strong",null,e.username),p["default"].createElement("ul",{className:"list-unstyled list-inline user-stats"},p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"message"),e.posts),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"forum"),e.threads),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"favorite"),e.followers),p["default"].createElement("li",null,p["default"].createElement("span",{className:"material-icon"},"favorite_outline"),e.following))),p["default"].createElement("li",{className:"divider"}),p["default"].createElement("li",null,p["default"].createElement("a",{href:e.url},p["default"].createElement("span",{className:"material-icon"},"account_circle"),gettext("See your profile"))),p["default"].createElement("li",null,p["default"].createElement("a",{href:w["default"].get("USERCP_URL")},p["default"].createElement("span",{className:"material-icon"},"done_all"),gettext("Change options"))),p["default"].createElement("li",null,p["default"].createElement("button",{className:"btn-link",onClick:this.changeAvatar,type:"button"},p["default"].createElement("span",{className:"material-icon"},"portrait"),gettext("Change avatar"))),!!e.acl.can_use_private_threads&&p["default"].createElement("li",null,p["default"].createElement("a",{href:w["default"].get("PRIVATE_THREADS_URL")},p["default"].createElement("span",{className:"material-icon"},"message"),gettext("Private threads"),p["default"].createElement(s,{user:e}))),p["default"].createElement("li",{className:"divider"}),p["default"].createElement("li",{className:"dropdown-buttons"},p["default"].createElement("button",{className:"btn btn-default btn-block",onClick:this.logout,type:"button"},gettext("Log out"))))}}]),t}(p["default"].Component);a.CompactUserNav=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),d(t,[{key:"showUserMenu",value:function(){k["default"].showConnected("user-menu",(0,m.connect)(c)(P))}},{key:"render",value:function(){return p["default"].createElement("button",{type:"button",onClick:this.showUserMenu},p["default"].createElement(b["default"],{user:this.props.user,size:"50"}))}}]),t}(p["default"].Component)},{"../..":301,"../../services/mobile-navbar-dropdown":369,"../../services/modal":370,"../avatar":6,"../change-avatar/root":25,"../navbar-search":76,react:"react","react-redux":"react-redux"}],273:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){var t="";return e.is_banned?t="banned":e.is_hidden?t="offline":e.is_online_hidden?t="online":e.is_offline_hidden?t="offline":e.is_online?t="online":e.is_offline&&(t="offline"),"user-status user-"+t}function i(e,t){return t.is_banned?t.banned_until?interpolate(gettext("%(username)s is banned until %(ban_expires)s"),{username:e.username,ban_expires:t.banned_until.format("LL, LT")},!0):interpolate(gettext("%(username)s is banned"),{username:e.username},!0):t.is_hidden?interpolate(gettext("%(username)s is hiding presence"),{username:e.username},!0):t.is_online_hidden?interpolate(gettext("%(username)s is online (hidden)"),{username:e.username},!0):t.is_offline_hidden?interpolate(gettext("%(username)s was last seen %(last_click)s (hidden)"),{username:e.username,last_click:t.last_click.fromNow()},!0):t.is_online?interpolate(gettext("%(username)s is online"),{username:e.username},!0):t.is_offline?interpolate(gettext("%(username)s was last seen %(last_click)s"),{username:e.username,last_click:t.last_click.fromNow()},!0):void 0}Object.defineProperty(a,"__esModule",{value:!0}),a.StatusLabel=a.StatusIcon=void 0;var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.getStatusClassName=s,a.getStatusDescription=i;var c=e("react"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getClass",value:function(){return s(this.props.status)}},{key:"render",value:function(){return d["default"].createElement("span",{className:this.getClass()},this.props.children)}}]),t}(d["default"].Component);a["default"]=f;a.StatusIcon=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getIcon",value:function(){return this.props.status.is_banned?"remove_circle_outline":this.props.status.is_hidden?"help_outline":this.props.status.is_online_hidden?"label":this.props.status.is_offline_hidden?"label_outline":this.props.status.is_online?"lens":this.props.status.is_offline?"panorama_fish_eye":void 0}},{key:"render",value:function(){return d["default"].createElement("span",{className:"material-icon status-icon"},this.getIcon())}}]),t}(d["default"].Component),a.StatusLabel=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"getHelp",value:function(){return i(this.props.user,this.props.status)}},{key:"getLabel",value:function(){return this.props.status.is_banned?gettext("Banned"):this.props.status.is_hidden?gettext("Hidden"):this.props.status.is_online_hidden?gettext("Online (hidden)"):this.props.status.is_offline_hidden?gettext("Offline (hidden)"):this.props.status.is_online?gettext("Online"):this.props.status.is_offline?gettext("Offline"):void 0}},{key:"render",value:function(){return d["default"].createElement("span",{className:this.props.className||"status-label",title:this.getHelp()},this.getLabel())}}]),t}(d["default"].Component)},{react:"react"}],274:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../avatar"),f=r(d),p=e("../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"getClassName",value:function(){return this.props.hiddenOnMobile?"list-group-item hidden-xs hidden-sm":"list-group-item"}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"change-avatar"},c["default"].createElement("span",{className:"user-avatar"},c["default"].createElement(f["default"],{size:"100"}))),c["default"].createElement("div",{className:"change-author"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,100)+"px"}}," ")),c["default"].createElement("div",{className:"change"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," "),c["default"].createElement("span",{className:"material-icon"},"arrow_forward"),c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("div",{className:"change-date"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](80,140)+"px"}}," ")))}}]),t}(c["default"].Component);a["default"]=h},{"../../utils/random":387,"../avatar":6,react:"react"}],275:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../avatar"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"renderUserAvatar",value:function(){return this.props.change.changed_by?u["default"].createElement("a",{href:this.props.change.changed_by.url,className:"user-avatar-wrapper"},u["default"].createElement(d["default"],{user:this.props.change.changed_by,size:"100"})):u["default"].createElement("span",{className:"user-avatar-wrapper"},u["default"].createElement(d["default"],{size:"100"}))}},{key:"renderUsername",value:function(){return this.props.change.changed_by?u["default"].createElement("a",{href:this.props.change.changed_by.url,className:"item-title"},this.props.change.changed_by.username):u["default"].createElement("span",{className:"item-title"},this.props.change.changed_by_username)}},{key:"render",value:function(){return u["default"].createElement("li",{className:"list-group-item",key:this.props.change.id},u["default"].createElement("div",{className:"change-avatar"},this.renderUserAvatar()),u["default"].createElement("div",{className:"change-author"},this.renderUsername()),u["default"].createElement("div",{className:"change"},u["default"].createElement("span",{className:"old-username"},this.props.change.old_username),u["default"].createElement("span",{className:"material-icon"},"arrow_forward"),u["default"].createElement("span",{className:"new-username"},this.props.change.new_username)),u["default"].createElement("div",{className:"change-date"},u["default"].createElement("abbr",{title:this.props.change.changed_on.format("LLL")},this.props.change.changed_on.fromNow())))}}]),t}(u["default"].Component);a["default"]=f},{"../avatar":6,react:"react"}],276:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getEmptyMessage",value:function(){return this.props.emptyMessage?this.props.emptyMessage:gettext("No name changes have been recorded for your account.")}},{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-ready"},u["default"].createElement("ul",{className:"list-group"},u["default"].createElement("li",{className:"list-group-item empty-message"},this.getEmptyMessage())))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],277:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change-preview"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-preview"},u["default"].createElement("ul",{className:"list-group"},[0,1,2].map(function(e){return u["default"].createElement(d["default"],{hiddenOnMobile:e>0,key:e})})))}}]),t}(u["default"].Component);a["default"]=f},{"./change-preview":274,react:"react"}],278:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./change"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return u["default"].createElement("div",{className:"username-history ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.props.changes.map(function(e){return u["default"].createElement(d["default"],{change:e,key:e.id})})))}}]),t}(u["default"].Component);a["default"]=f},{"./change":275,react:"react"}],279:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-empty"),d=n(c),f=e("./list-ready"),p=n(f),m=e("./list-preview"),h=n(m),b=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"render",value:function(){return this.props.isLoaded?this.props.changes.length?u["default"].createElement(p["default"],{changes:this.props.changes}):u["default"].createElement(d["default"],{emptyMessage:this.props.emptyMessage}):u["default"].createElement(h["default"],null)}}]),t}(u["default"].Component);a["default"]=b},{"./list-empty":276,"./list-preview":277,"./list-ready":278,react:"react"}],280:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.showStatus,a=e.user,n=a.rank,r="panel user-card";return n.css_class&&(r+=" user-card-"+n.css_class),o["default"].createElement("div",{className:r},o["default"].createElement("div",{className:"panel-body"},o["default"].createElement("div",{className:"row"},o["default"].createElement("div",{className:"col-xs-3 user-card-left"},o["default"].createElement("div",{className:"user-card-small-avatar"},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{size:"50",size2x:"80",user:a})))),o["default"].createElement("div",{className:"col-xs-9 col-sm-12 user-card-body"},o["default"].createElement("div",{className:"user-card-avatar"},o["default"].createElement("a",{href:a.url},o["default"].createElement(s["default"],{size:"150",size2x:"200",user:a}))),o["default"].createElement("div",{className:"user-card-username"},o["default"].createElement("a",{href:a.url},a.username)),o["default"].createElement("div",{className:"user-card-title"},o["default"].createElement(d["default"],{rank:n,title:a.title})),o["default"].createElement("div",{className:"user-card-stats"},o["default"].createElement(u["default"],{showStatus:t,user:a}))))))};var r=e("react"),o=n(r),l=e("../../avatar"),s=n(l),i=e("./stats"),u=n(i),c=e("./user-title"),d=n(c)},{"../../avatar":6,"./stats":281,"./user-title":282,react:"react"}],281:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=e.showStatus,a=e.user;return t?d["default"].createElement("li",{className:"user-stat-status"},d["default"].createElement(p["default"],{status:a.status},d["default"].createElement(f.StatusLabel,{status:a.status,user:a}))):null}function o(e){var t=e.user,a=t.joined_on,n=interpolate(gettext("Joined on %(joined_on)s"),{joined_on:a.format("LL, LT")},!0),r=interpolate(gettext("Joined %(joined_on)s"),{joined_on:a.fromNow()},!0);return d["default"].createElement("li",{className:"user-stat-join-date"},d["default"].createElement("abbr",{title:n},r))}function l(e){var t=e.user,a=u("user-stat-posts",t.posts),n=ngettext("%(posts)s post","%(posts)s posts",t.posts);return d["default"].createElement("li",{className:a},interpolate(n,{posts:t.posts},!0))}function s(e){var t=e.user,a=u("user-stat-threads",t.threads),n=ngettext("%(threads)s thread","%(threads)s threads",t.threads);return d["default"].createElement("li",{className:a},interpolate(n,{threads:t.threads},!0))}function i(e){var t=e.user,a=u("user-stat-followers",t.followers),n=ngettext("%(followers)s follower","%(followers)s followers",t.followers);return d["default"].createElement("li",{className:a},interpolate(n,{followers:t.followers},!0))}function u(e,t){return 0===t?e+" user-stat-empty":e}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.showStatus,a=e.user;return d["default"].createElement("ul",{className:"list-unstyled"},d["default"].createElement(r,{showStatus:t,user:a}),d["default"].createElement(o,{user:a}),d["default"].createElement("li",{className:"user-stat-divider"}),d["default"].createElement(l,{user:a}),d["default"].createElement(s,{user:a}),d["default"].createElement(i,{user:a}))},a.Status=r,a.JoinDate=o,a.Posts=l,a.Threads=s,a.Followers=i,a.getStatClassName=u;var c=e("react"),d=n(c),f=e("../../user-status"),p=n(f)},{"../../user-status":273,react:"react"}],282:[function(e,t,a){arguments[4][129][0].apply(a,arguments)},{dup:129,react:"react"}],283:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.cols,a=e.isReady,n=e.showStatus,r=e.users,l="col-xs-12 col-sm-4";return 4===t&&(l+=" col-md-3"),a?o["default"].createElement("div",{className:"users-cards-list ui-ready"},o["default"].createElement("div",{className:"row"},r.map(function(e){return o["default"].createElement("div",{className:l,key:e.id},o["default"].createElement(s["default"],{showStatus:n,user:e}))}))):o["default"].createElement(u["default"],{colClassName:l,cols:t})};var r=e("react"),o=n(r),l=e("./card"),s=n(l),i=e("./preview"),u=n(i)},{"./card":280,"./preview":285,react:"react"}],284:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../avatar"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("div",{className:"panel user-card user-card-preview"},c["default"].createElement("div",{className:"panel-body"},c["default"].createElement("div",{className:"row"},c["default"].createElement("div",{className:"col-xs-3 user-card-left"},c["default"].createElement("div",{className:"user-card-small-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"50",size2x:"80"})))),c["default"].createElement("div",{className:"col-xs-9 col-sm-12 user-card-body"},c["default"].createElement("div",{className:"user-card-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"150",size2x:"200"}))),c["default"].createElement("div",{className:"user-card-username"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](60,150)+"px"}}," ")),c["default"].createElement("div",{className:"user-card-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](60,150)+"px"}}," ")),c["default"].createElement("div",{className:"user-card-stats"},c["default"].createElement("ul",{className:"list-unstyled"},c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",{className:"user-stat-divider"}),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," ")),c["default"].createElement("li",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,70)+"px"}}," "))))))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":387,"../../avatar":6,react:"react"}],285:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.colClassName,a=e.cols,n=Array.apply(null,{length:a}).map(Number.call,Number);return o["default"].createElement("div",{className:"users-cards-list ui-preview"},o["default"].createElement("div",{className:"row"},n.map(function(e){var a=t;return 0!==e&&(a+=" hidden-xs"),3===e&&(a+=" hidden-sm"),o["default"].createElement("div",{className:a,key:e},o["default"].createElement(s["default"],null))})))};var r=e("react"),o=n(r),l=e("./card"),s=n(l)},{"./card":284,react:"react"}],286:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getEmptyMessage",value:function(){return interpolate(gettext("No users have posted any new messages during last %(days)s days."),{days:this.props.trackedPeriod},!0)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"active-posters-list"},u["default"].createElement("div",{className:"container"},u["default"].createElement("p",{className:"lead"},this.getEmptyMessage())))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],287:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("../../avatar"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"getClassName",value:function(){return this.props.hiddenOnMobile?"list-group-item hidden-xs hidden-sm":"list-group-item"}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"rank-user-avatar"},c["default"].createElement("span",null,c["default"].createElement(f["default"],{size:"50"}))),c["default"].createElement("div",{className:"rank-user"},c["default"].createElement("div",{className:"user-name"},c["default"].createElement("span",{className:"item-title"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,80)+"px"}}," "))),c["default"].createElement("div",{className:"user-details"},c["default"].createElement("span",{className:"user-status"},c["default"].createElement("span",{className:"status-icon ui-preview-text"}," "),c["default"].createElement("span",{className:"status-label ui-preview-text hidden-xs hidden-sm",style:{width:m["int"](30,50)+"px"}}," ")),c["default"].createElement("span",{className:"rank-name"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,50)+"px"}}," ")),c["default"].createElement("span",{className:"user-title hidden-xs hidden-sm"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](30,50)+"px"}}," "))),c["default"].createElement("div",{className:"user-compact-stats visible-xs-block"},c["default"].createElement("span",{className:"rank-position"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("span",{className:"rank-posts-counted"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Ranked posts"))))),c["default"].createElement("div",{className:"rank-position hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("div",{className:"rank-posts-counted hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",
+style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Ranked posts"))),c["default"].createElement("div",{className:"rank-posts-total hidden-xs"},c["default"].createElement("strong",null,c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](20,30)+"px"}}," ")),c["default"].createElement("small",null,gettext("Total posts"))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":387,"../../avatar":6,react:"react"}],288:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("react-router"),f=e("../../avatar"),p=r(f),m=e("../../user-status"),h=r(m),b=e("../../../index"),v=r(b),g=e("../../../utils/random"),_=n(g),y=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"getClassName",value:function(){return this.props.rank.css_class?"list-group-item list-group-rank-"+this.props.rank.css_class:"list-group-item"}},{key:"getUserStatus",value:function(){return this.props.user.status?c["default"].createElement(h["default"],{user:this.props.user,status:this.props.user.status},c["default"].createElement(m.StatusIcon,{user:this.props.user,status:this.props.user.status}),c["default"].createElement(m.StatusLabel,{user:this.props.user,status:this.props.user.status,className:"status-label hidden-xs hidden-sm"})):c["default"].createElement("span",{className:"user-status"},c["default"].createElement("span",{className:"status-icon ui-preview-text"}," "),c["default"].createElement("span",{className:"status-label ui-preview-text hidden-xs hidden-sm",style:{width:_["int"](30,50)+"px"}}," "))}},{key:"getRankName",value:function(){if(!this.props.rank.is_tab)return c["default"].createElement("span",{className:"rank-name item-title"},this.props.rank.name);var e=v["default"].get("USERS_LIST_URL")+this.props.rank.slug+"/";return c["default"].createElement(d.Link,{to:e,className:"rank-name item-title"},this.props.rank.name)}},{key:"getUserTitle",value:function(){return this.props.user.title?c["default"].createElement("span",{className:"user-title hidden-xs hidden-sm"},this.props.user.title):null}},{key:"render",value:function(){return c["default"].createElement("li",{className:this.getClassName()},c["default"].createElement("div",{className:"rank-user-avatar"},c["default"].createElement("a",{href:this.props.user.url},c["default"].createElement(p["default"],{user:this.props.user,size:50,size2x:64}))),c["default"].createElement("div",{className:"rank-user"},c["default"].createElement("div",{className:"user-name"},c["default"].createElement("a",{href:this.props.user.url,className:"item-title"},this.props.user.username)),c["default"].createElement("div",{className:"user-details"},this.getUserStatus(),this.getRankName(),this.getUserTitle()),c["default"].createElement("div",{className:"user-compact-stats visible-xs-block"},c["default"].createElement("span",{className:"rank-position"},c["default"].createElement("strong",null,"#",this.props.counter),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("span",{className:"rank-posts-counted"},c["default"].createElement("strong",null,this.props.user.meta.score),c["default"].createElement("small",null,gettext("Ranked posts"))))),c["default"].createElement("div",{className:"rank-position hidden-xs"},c["default"].createElement("strong",null,"#",this.props.counter),c["default"].createElement("small",null,gettext("Rank"))),c["default"].createElement("div",{className:"rank-posts-counted hidden-xs"},c["default"].createElement("strong",null,this.props.user.meta.score),c["default"].createElement("small",null,gettext("Ranked posts"))),c["default"].createElement("div",{className:"rank-posts-total hidden-xs"},c["default"].createElement("strong",null,this.props.user.posts),c["default"].createElement("small",null,gettext("Total posts"))))}}]),t}(c["default"].Component);a["default"]=y},{"../../../index":301,"../../../utils/random":387,"../../avatar":6,"../../user-status":273,react:"react","react-router":"react-router"}],289:[function(e,t,a){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&(t[a]=e[a]);return t["default"]=e,t}function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=r(u),d=e("./list-item-preview"),f=r(d),p=e("../../../utils/random"),m=n(p),h=function(e){function t(){return o(this,t),l(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return s(t,e),i(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return c["default"].createElement("div",{className:"active-posters-list"},c["default"].createElement("div",{className:"container"},c["default"].createElement("p",{className:"lead ui-preview"},c["default"].createElement("span",{className:"ui-preview-text",style:{width:m["int"](50,220)+"px"}}," ")),c["default"].createElement("div",{className:"active-posters ui-preview"},c["default"].createElement("ul",{className:"list-group"},[0,1,2].map(function(e){return c["default"].createElement(f["default"],{hiddenOnMobile:e>0,key:e})})))))}}]),t}(c["default"].Component);a["default"]=h},{"../../../utils/random":387,"./list-item-preview":287,react:"react"}],290:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-item"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"getLeadMessage",value:function(){var e=ngettext("%(posters)s most active poster from last %(days)s days.","%(posters)s most active posters from last %(days)s days.",this.props.count);return interpolate(e,{posters:this.props.count,days:this.props.trackedPeriod},!0)}},{key:"render",value:function(){return u["default"].createElement("div",{className:"active-posters-list"},u["default"].createElement("div",{className:"container"},u["default"].createElement("p",{className:"lead"},this.getLeadMessage()),u["default"].createElement("div",{className:"active-posters ui-ready"},u["default"].createElement("ul",{className:"list-group"},this.props.users.map(function(e,t){return u["default"].createElement(d["default"],{user:e,rank:e.rank,counter:t+1,key:e.id})})))))}}]),t}(u["default"].Component);a["default"]=f},{"./list-item":288,react:"react"}],291:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("./list-empty"),d=n(c),f=e("./list-preview"),p=n(f),m=e("./list-ready"),h=n(m),b=e("../../../index"),v=n(b),g=e("../../../reducers/users"),_=e("../../../services/polls"),y=n(_),E=e("../../../services/store"),w=n(E),O=e("../../../services/page-title"),k=n(O),N=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){w["default"].dispatch((0,g.hydrate)(e.results)),a.setState({isLoaded:!0,trackedPeriod:e.tracked_period,count:e.count})},v["default"].has("USERS")?a.initWithPreloadedData(v["default"].pop("USERS")):a.initWithoutPreloadedData(),a.startPolling(),a}return l(t,e),s(t,[{key:"initWithPreloadedData",value:function(e){this.state={isLoaded:!0,trackedPeriod:e.tracked_period,count:e.count},w["default"].dispatch((0,g.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(){y["default"].start({poll:"active-posters",url:v["default"].get("USERS_API"),data:{list:"active"},frequency:9e4,update:this.update})}},{key:"componentDidMount",value:function(){k["default"].set({title:this.props.route.extra.name,parent:gettext("Users")})}},{key:"componentWillUnmount",value:function(){y["default"].stop("active-posters")}},{key:"render",value:function(){return this.state.isLoaded?this.state.count>0?u["default"].createElement(h["default"],{users:this.props.users,trackedPeriod:this.state.trackedPeriod,count:this.state.count}):u["default"].createElement(d["default"],{trackedPeriod:this.state.trackedPeriod}):u["default"].createElement(p["default"],null)}}]),t}(u["default"].Component);a["default"]=N},{"../../../index":301,"../../../reducers/users":363,"../../../services/page-title":372,"../../../services/polls":373,"../../../services/store":376,"./list-empty":286,"./list-preview":289,"./list-ready":290,react:"react"}],292:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t=e.baseUrl,a=e.lists;return o["default"].createElement("ul",{className:"nav nav-pills"},a.map(function(e){var a=c(t,e);return o["default"].createElement(i["default"],{path:a,key:a},o["default"].createElement(l.Link,{to:a},e.name))}))};var r=e("react"),o=n(r),l=e("react-router"),s=e("../li"),i=n(s),u=e("../../index"),c=(n(u),function(e,t){var a=e;return a+="rank"===t.component?t.slug:t.component,a+"/"})},{"../../index":301,"../li":56,react:"react","react-router":"react-router"}],293:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../../users-list"),d=n(c),f=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"shouldComponentUpdate",value:function(){return!1}},{key:"render",value:function(){return u["default"].createElement("div",null,u["default"].createElement(d["default"],{cols:4,isReady:!1}))}}]),t}(u["default"].Component);a["default"]=f},{"../../users-list":283,react:"react"}],294:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return o["default"].createElement("div",null,o["default"].createElement(u["default"],{cols:4,isReady:!0,showStatus:!0,users:e.users}),o["default"].createElement(s["default"],e))};var r=e("react"),o=n(r),l=e("./pager"),s=n(l),i=e("../../users-list"),u=n(i)},{"../../users-list":283,"./pager":295,react:"react"}],295:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return d["default"].createElement("div",{className:"row row-paginator"},d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(o,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(l,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(s,e)),d["default"].createElement("div",{className:"col-xs-3"},d["default"].createElement(i,e)))}function o(e){return e.isLoaded&&e.first?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl,title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to first page")},d["default"].createElement("span",{className:"material-icon"},"first_page"))}function l(e){if(e.isLoaded&&e.page>1){var t="";return e.previous&&(t=e.previous+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+t,title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to previous page")},d["default"].createElement("span",{className:"material-icon"},"chevron_left"))}function s(e){if(e.isLoaded&&e.more){var t="";return e.next&&(t=e.next+"/"),d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+t,title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}return d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to next page")},d["default"].createElement("span",{className:"material-icon"},"chevron_right"))}function i(e){return e.isLoaded&&e.last?d["default"].createElement(f.Link,{className:"btn btn-default btn-block btn-icon btn-outline",onClick:m["default"],to:e.baseUrl+e.last+"/",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page")):d["default"].createElement("span",{className:"btn btn-default btn-block btn-icon btn-outline disabled",title:gettext("Go to last page")},d["default"].createElement("span",{className:"material-icon"},"last_page"))}function u(e){var t=null;return e.more?(t=ngettext("There is %(more)s more member with this role.","There are %(more)s more members with this role.",e.more),t=interpolate(t,{more:e.more},!0)):t=gettext("There are no more members with this role."),d["default"].createElement("p",null,t)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return 1===e.pages?null:d["default"].createElement("div",{className:"row row-toolbar"},d["default"].createElement("div",{className:"col-xs-12 text-center visible-xs-block"},d["default"].createElement(u,{more:e.more}),d["default"].createElement("div",{className:"toolbar-vertical-spacer"})),d["default"].createElement("div",{className:"col-md-7"},d["default"].createElement("div",{className:"row"},d["default"].createElement("div",{className:"col-sm-4 col-md-5"},d["default"].createElement(r,e)),d["default"].createElement("div",{className:"col-sm-8 col-md-7 hidden-xs"},d["default"].createElement(u,{more:e.more})))))},a.Pager=r,a.FirstPage=o,a.PreviousPage=l,a.NextPage=s,a.LastPage=i,a.More=u;var c=e("react"),d=n(c),f=e("react-router"),p=e("../../../utils/reset-scroll"),m=n(p)},{"../../../utils/reset-scroll":388,react:"react","react-router":"react-router"}],296:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},i=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),u=e("react"),c=n(u),d=e("../../page-lead"),f=n(d),p=e("./list"),m=n(p),h=e("./list-loading"),b=n(h),v=e("../../../index"),g=n(v),_=e("../../../reducers/users"),y=e("../../../services/polls"),E=n(y),w=e("../../../services/store"),O=n(w),k=e("../../../services/page-title"),N=n(k),x=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.update=function(e){O["default"].dispatch((0,_.hydrate)(e.results)),e.isLoaded=!0,a.setState(e)},g["default"].has("USERS")?a.initWithPreloadedData(g["default"].pop("USERS")):a.initWithoutPreloadedData(),a.startPolling(e.params.page||1),a}return l(t,e),i(t,[{key:"initWithPreloadedData",value:function(e){this.state=Object.assign(e,{isLoaded:!0}),O["default"].dispatch((0,_.hydrate)(e.results))}},{key:"initWithoutPreloadedData",value:function(){this.state={isLoaded:!1}}},{key:"startPolling",value:function(e){E["default"].start({poll:"rank-users",url:g["default"].get("USERS_API"),data:{rank:this.props.route.rank.id,page:e},frequency:9e4,update:this.update})}},{key:"componentDidMount",value:function(){N["default"].set({title:this.props.route.rank.name,page:this.props.params.page||null,parent:gettext("Users")})}},{key:"componentWillUnmount",value:function(){E["default"].stop("rank-users")}},{key:"componentWillReceiveProps",value:function(e){this.props.params.page!==e.params.page&&(N["default"].set({title:this.props.route.rank.name,page:e.params.page||null,parent:gettext("Users")}),this.setState({isLoaded:!1}),E["default"].stop("rank-users"),this.startPolling(e.params.page))}},{key:"getClassName",value:function(){return this.props.route.rank.css_class?"rank-users-list rank-users-"+this.props.route.rank.css_class:"rank-users-list"}},{key:"getRankDescription",value:function(){return this.props.route.rank.description?c["default"].createElement("div",{className:"rank-description"},c["default"].createElement(f["default"],{copy:this.props.route.rank.description.html})):null}},{key:"getComponent",value:function(){if(this.state.isLoaded){if(this.state.count>0){var e=g["default"].get("USERS_LIST_URL")+this.props.route.rank.slug+"/";return c["default"].createElement(m["default"],s({baseUrl:e,users:this.props.users},this.state))}return c["default"].createElement("p",{className:"lead"},gettext("There are no users with this rank at the moment."))}return c["default"].createElement(b["default"],null)}},{key:"render",value:function(){return c["default"].createElement("div",{className:this.getClassName()},c["default"].createElement("div",{className:"container"},this.getRankDescription(),this.getComponent()))}}]),t}(c["default"].Component);a["default"]=x},{"../../../index":301,"../../../reducers/users":363,"../../../services/page-title":372,"../../../services/polls":373,"../../../services/store":376,"../../page-lead":91,"./list":294,"./list-loading":293,react:"react"}],297:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{tick:e.tick.tick,user:e.auth.user,users:e.users}}function i(){var e=[];return O["default"].get("USERS_LISTS").forEach(function(t){"rank"===t.component?(e.push({path:O["default"].get("USERS_LIST_URL")+t.slug+"/:page/",component:(0,f.connect)(s)(_["default"]),rank:t}),e.push({path:O["default"].get("USERS_LIST_URL")+t.slug+"/",component:(0,f.connect)(s)(_["default"]),rank:t})):"active-posters"===t.component&&e.push({path:O["default"].get("USERS_LIST_URL")+t.component+"/",component:(0,f.connect)(s)(v["default"]),extra:{name:t.name}})}),e}Object.defineProperty(a,"__esModule",{value:!0});var u=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.select=s,a.paths=i;var c=e("react"),d=n(c),f=e("react-redux"),p=e("../dropdown-toggle"),m=(n(p),e("./nav")),h=n(m),b=e("./active-posters/root"),v=n(b),g=e("./rank/root"),_=n(g),y=e("../with-dropdown"),E=n(y),w=e("../../index"),O=n(w),k=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),u(t,[{key:"render",value:function(){return d["default"].createElement("div",{className:"page page-users-lists"},d["default"].createElement("div",{className:"page-header-bg"},d["default"].createElement("div",{className:"page-header"},d["default"].createElement("div",{className:"container"},d["default"].createElement("h1",null,gettext("Users"))),d["default"].createElement("div",{className:"page-tabs"},d["default"].createElement("div",{className:"container"},d["default"].createElement(h["default"],{lists:O["default"].get("USERS_LISTS"),baseUrl:O["default"].get("USERS_LIST_URL")}))))),this.props.children)}}]),t}(E["default"]);a["default"]=k},{"../../index":301,"../dropdown-toggle":27,"../with-dropdown":298,"./active-posters/root":291,"./nav":292,"./rank/root":296,react:"react","react-redux":"react-redux"}],298:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(e){r(this,t);var a=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return a.toggleNav=function(){a.setState({dropdown:!a.state.dropdown})},a.hideNav=function(){a.setState({dropdown:!1})},a.state={dropdown:!1},a}return l(t,e),s(t,[{key:"getCompactNavClassName",value:function(){return this.state.dropdown?"compact-nav open":"compact-nav"}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],299:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u<s;u++)i[u]=arguments[u];return a=n=o(this,(e=t.__proto__||Object.getPrototypeOf(t)).call.apply(e,[this].concat(i))),n.toggle=function(){n.props.onChange({target:{value:!n.props.value}})},l=a,o(n,l)}return l(t,e),s(t,[{key:"getClassName",value:function(){return this.props.value?"btn btn-yes-no btn-yes-no-on":"btn btn-yes-no btn-yes-no-off"}},{key:"getIcon",value:function(){return this.props.value?this.props.iconOn||"check_box":this.props.iconOff||"check_box_outline_blank"}},{key:"getLabel",value:function(){return this.props.value?this.props.labelOn||gettext("yes"):this.props.labelOff||gettext("no")}},{key:"render",value:function(){return u["default"].createElement("button",{type:"button",onClick:this.toggle,className:this.getClassName(),id:this.props.id||null,"aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},u["default"].createElement("span",{className:"material-icon"},this.getIcon()),u["default"].createElement("span",{className:"btn-text"},this.getLabel()))}}]),t}(u["default"].Component);a["default"]=c},{react:"react"}],300:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../reducers/profile-details"),d=e("../services/ajax"),f=n(d),p=e("../services/snackbar"),m=n(p),h=function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return l(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this.props,t=e.data,a=e.dispatch,n=e.user;t&&t.id===n.id||f["default"].get(this.props.user.api.details).then(function(e){a((0,c.load)(e))},function(e){m["default"].apiError(e)})}},{key:"render",value:function(){return this.props.children}}]),t}(u["default"].Component);a["default"]=h},{"../reducers/profile-details":354,"../services/ajax":364,"../services/snackbar":375,react:"react"}],301:[function(e,t,a){(function(t){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Misago=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("./utils/ordered-list"),s=n(l),i=a.Misago=function(){function e(){r(this,e),this._initializers=[],this._context={}}return o(e,[{key:"addInitializer",value:function(e){this._initializers.push({key:e.name,item:e.initializer,after:e.after,before:e.before})}},{key:"init",value:function(e){var t=this;this._context=e;var a=new s["default"](this._initializers).orderedValues();a.forEach(function(e){e(t)})}},{key:"has",value:function(e){return!!this._context[e]}},{key:"get",value:function(e,t){return this.has(e)?this._context[e]:t||void 0}},{key:"pop",value:function(e){if(this.has(e)){var t=this._context[e];return this._context[e]=null,t}}}]),e}(),u=new i;t.misago=u,a["default"]=u}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./utils/ordered-list":386}],302:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(l["default"].get("CSRF_COOKIE_NAME"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s);l["default"].addInitializer({
+name:"ajax",initializer:r})},{"../index":301,"../services/ajax":364}],303:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.get("isAuthenticated")&&window.setInterval(function(){u["default"].get(e.get("AUTH_API")).then(function(e){p["default"].dispatch((0,s.patch)(e))},function(e){d["default"].apiError(e)})},1e3*m)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../reducers/auth"),i=e("../services/ajax"),u=n(i),c=e("../services/snackbar"),d=n(c),f=e("../services/store"),p=n(f),m=45;l["default"].addInitializer({name:"auth-sync",initializer:r,after:"auth"})},{"../index":301,"../reducers/auth":349,"../services/ajax":364,"../services/snackbar":375,"../services/store":376}],304:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(f["default"],m["default"],c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/auth"),i=n(s),u=e("../services/modal"),c=n(u),d=e("../services/store"),f=n(d),p=e("../services/local-storage"),m=n(p);l["default"].addInitializer({name:"auth",initializer:r,after:"store"})},{"../index":301,"../services/auth":365,"../services/local-storage":368,"../services/modal":370,"../services/store":376}],305:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){c["default"].init(e,i["default"],f["default"],m["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/captcha"),c=n(u),d=e("../services/include"),f=n(d),p=e("../services/snackbar"),m=n(p);l["default"].addInitializer({name:"captcha",initializer:r})},{"../index":301,"../services/ajax":364,"../services/captcha":366,"../services/include":367,"../services/snackbar":375}],306:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){document.getElementById("required-agreement-mount")&&(0,f["default"])(l["default"].createElement(c["default"],{api:e.get("REQUIRED_AGREEMENT_API")}),"required-agreement-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react"),l=n(o),s=e("../../index"),i=n(s),u=e("../../components/accept-agreement"),c=n(u),d=e("../../utils/mount-component"),f=n(d);i["default"].addInitializer({name:"component:accept-agreement",initializer:r,after:"store"})},{"../../components/accept-agreement":3,"../../index":301,"../../utils/mount-component":385,react:"react"}],307:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,d["default"])((0,o.connect)(i.select)(u["default"]),"auth-message-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/auth-message"),u=n(i),c=e("../../utils/mount-component"),d=n(c);s["default"].addInitializer({name:"component:auth-message",initializer:r,after:"store"})},{"../../components/auth-message":5,"../../index":301,"../../utils/mount-component":385,"react-redux":"react-redux"}],308:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("BAN_MESSAGE")&&(0,i["default"])(e.get("BAN_MESSAGE"),!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../utils/banned-page"),i=n(s);l["default"].addInitializer({name:"component:banmed-page",initializer:r,after:"store"})},{"../../index":301,"../../utils/banned-page":378}],309:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("categories-mount")&&(0,d["default"])((0,o.connect)(l.select)(s["default"]),"categories-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../components/categories"),s=n(l),i=e("../../index"),u=n(i),c=e("../../utils/mount-component"),d=n(c);u["default"].addInitializer({name:"component:categories",initializer:r,after:"store"})},{"../../components/categories":20,"../../index":301,"../../utils/mount-component":385,"react-redux":"react-redux"}],310:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("USER_OPTIONS")&&(0,c["default"])({root:i["default"].get("USERCP_URL"),component:l["default"],paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/options/root"),l=n(o),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:options",initializer:r,after:"store"})},{"../../components/options/root":86,"../../index":301,"../../utils/routed-component":389}],311:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("PROFILE")&&e.has("PROFILE_PAGES")&&(0,d["default"])({root:u["default"].get("PROFILE").url,component:(0,o.connect)(l.select)(s["default"]),paths:(0,l.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../components/profile/root"),s=n(l),i=e("../../index"),u=n(i),c=e("../../utils/routed-component"),d=n(c);u["default"].addInitializer({name:"component:profile",initializer:r,after:"reducer:profile-hydrate"})},{"../../components/profile/root":194,"../../index":301,"../../utils/routed-component":389,"react-redux":"react-redux"}],312:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("request-activation-link-mount")&&(0,c["default"])(i["default"],"request-activation-link-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../components/request-activation-link"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:request-activation-link",initializer:r,after:"store"})},{"../../components/request-activation-link":199,"../../index":301,"../../utils/mount-component":385}],313:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("request-password-reset-mount")&&(0,c["default"])(i["default"],"request-password-reset-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../components/request-password-reset"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:request-password-reset",initializer:r,after:"store"})},{"../../components/request-password-reset":200,"../../index":301,"../../utils/mount-component":385}],314:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){document.getElementById("reset-password-form-mount")&&(0,c["default"])(i["default"],"reset-password-form-mount",!1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../.."),l=n(o),s=e("../../components/reset-password-form"),i=n(s),u=e("../../utils/mount-component"),c=n(u);l["default"].addInitializer({name:"component:reset-password-form",initializer:r,after:"store"})},{"../..":301,"../../components/reset-password-form":201,"../../utils/mount-component":385}],315:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){"misago:search"===e.get("CURRENT_LINK")&&(0,c["default"])({paths:(0,l["default"])(i["default"].get("SEARCH_PROVIDERS"))})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/search"),l=n(o),s=e("../.."),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:search",initializer:r,after:"store"})},{"../..":301,"../../components/search":203,"../../utils/routed-component":389}],316:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,c["default"])((0,o.connect)(i.select)(i.Snackbar),"snackbar-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/snackbar"),u=e("../../utils/mount-component"),c=n(u);s["default"].addInitializer({name:"component:snackbar",initializer:r,after:"snackbar"})},{"../../components/snackbar":211,"../../index":301,"../../utils/mount-component":385,"react-redux":"react-redux"}],317:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){if("social:complete"===e.get("CURRENT_LINK")){var t=e.get("SOCIAL_AUTH");(0,f["default"])(l["default"].createElement(i["default"],t),"page-mount")}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react"),l=n(o),s=e("../../components/social-auth"),i=n(s),u=e("../.."),c=n(u),d=e("../../utils/mount-component"),f=n(d);c["default"].addInitializer({name:"component:social-auth",initializer:r,after:"store"})},{"../..":301,"../../components/social-auth":214,"../../utils/mount-component":385,react:"react"}],318:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("THREAD")&&e.has("POSTS")&&(0,u["default"])({paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/thread/root"),l=e("../../index"),s=n(l),i=e("../../utils/routed-component"),u=n(i);s["default"].addInitializer({name:"component:thread",initializer:r,after:"store"})},{"../../components/thread/root":232,"../../index":301,"../../utils/routed-component":389}],319:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("THREADS")&&e.has("CATEGORIES")&&(0,c["default"])({paths:(0,l.paths)(e.get("user"),o(e))})}function o(e){var t=e.get("CURRENT_LINK");return t.substr(0,d.length)===d?{api:e.get("PRIVATE_THREADS_API"),startThread:{mode:"START_PRIVATE",submit:i["default"].get("PRIVATE_THREADS_API")},title:gettext("Private threads"),pageLead:gettext("Private threads are threads which only those that started them and those they have invited may see and participate in."),emptyMessage:gettext("You aren't participating in any private threads.")}:{api:e.get("THREADS_API")}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r,a.getListOptions=o;var l=e("../../components/threads/root"),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u),d="misago:private-threads";i["default"].addInitializer({name:"component:threads",initializer:r,after:"store"})},{"../../components/threads/root":266,"../../index":301,"../../utils/routed-component":389}],320:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){(0,c["default"])((0,o.connect)(i.select)(i.UserMenu),"user-menu-mount"),(0,c["default"])((0,o.connect)(i.select)(i.CompactUserMenu),"user-menu-compact-mount")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("react-redux"),l=e("../../index"),s=n(l),i=e("../../components/user-menu/root"),u=e("../../utils/mount-component"),c=n(u);s["default"].addInitializer({name:"component:user-menu",initializer:r,after:"store"})},{"../../components/user-menu/root":271,"../../index":301,"../../utils/mount-component":385,"react-redux":"react-redux"}],321:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){e.has("USERS_LISTS")&&(0,c["default"])({root:i["default"].get("USERS_LIST_URL"),component:l["default"],paths:(0,o.paths)()})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../components/users/root"),l=n(o),s=e("../../index"),i=n(s),u=e("../../utils/routed-component"),c=n(u);i["default"].addInitializer({name:"component:users",initializer:r,after:"store"})},{"../../components/users/root":297,"../../index":301,"../../utils/routed-component":389}],322:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){i["default"].init(e.get("STATIC_URL"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/include"),i=n(s);l["default"].addInitializer({name:"include",initializer:r})},{"../index":301,"../services/include":367}],323:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init("misago_")}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/local-storage"),i=n(s);l["default"].addInitializer({name:"local-storage",initializer:r})},{"../index":301,"../services/local-storage":368}],324:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=document.getElementById("mobile-navbar-dropdown-mount");e&&i["default"].init(e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/mobile-navbar-dropdown"),i=n(s);l["default"].addInitializer({name:"dropdown",initializer:r,before:"store"})},{"../index":301,"../services/mobile-navbar-dropdown":369}],325:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=document.getElementById("modal-mount");e&&i["default"].init(e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/modal"),i=n(s);l["default"].addInitializer({name:"modal",initializer:r,before:"store"})},{"../index":301,"../services/modal":370}],326:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){l["default"].locale($("html").attr("lang"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("moment"),l=n(o),s=e("../index"),i=n(s);i["default"].addInitializer({name:"moment",initializer:r})},{"../index":301,moment:"moment"}],327:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){i["default"].init(e.get("SETTINGS").forum_index_title,e.get("SETTINGS").forum_name)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/page-title"),i=n(s);l["default"].addInitializer({name:"page-title",initializer:r})},{"../index":301,"../services/page-title":372}],328:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){f["default"].init(i["default"],c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/snackbar"),c=n(u),d=e("../services/polls"),f=n(d);l["default"].addInitializer({name:"polls",initializer:r})},{"../index":301,"../services/ajax":364,"../services/polls":373,"../services/snackbar":375}],329:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].init(i["default"],f["default"],document.getElementById("posting-placeholder"))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/ajax"),i=n(s),u=e("../services/posting"),c=n(u),d=e("../services/snackbar"),f=n(d);l["default"].addInitializer({name:"posting",initializer:r})},{"../index":301,"../services/ajax":364,"../services/posting":374,"../services/snackbar":375}],330:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){c["default"].addReducer("auth",i["default"],Object.assign({isAuthenticated:e.get("isAuthenticated"),isAnonymous:!e.get("isAuthenticated"),user:e.get("user")},s.initialState))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/auth"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:auth",initializer:r,before:"store"})},{"../../index":301,"../../reducers/auth":349,"../../services/store":376}],331:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;l["default"].has("THREAD")&&(e=l["default"].get("THREAD").participants),c["default"].addReducer("participants",i["default"],e||[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/participants"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:participants",initializer:r,before:"store"})},{"../../index":301,"../../reducers/participants":350,"../../services/store":376}],332:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("THREAD")&&l["default"].get("THREAD").poll?(0,s.hydrate)(l["default"].get("THREAD").poll):{isBusy:!1},c["default"].addReducer("poll",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/poll"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:poll",initializer:r,before:"store"})},{"../../index":301,"../../reducers/poll":351,"../../services/store":376}],333:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("POSTS")?(0,s.hydrate)(l["default"].get("POSTS")):{isLoaded:!1,isBusy:!1},c["default"].addReducer("posts",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/posts"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:posts",initializer:r,before:"store"})},{"../../index":301,"../../reducers/posts":353,"../../services/store":376}],334:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;l["default"].has("PROFILE_DETAILS")&&(e=l["default"].get("PROFILE_DETAILS")),c["default"].addReducer("profile-details",i["default"],e||{})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile-details"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:profile-details",initializer:r,before:"store"})},{"../../index":301,"../../reducers/profile-details":354,"../../services/store":376}],335:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){l["default"].has("PROFILE")&&u["default"].dispatch((0,s.hydrate)(l["default"].get("PROFILE")))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile"),i=e("../../services/store"),u=n(i);l["default"].addInitializer({name:"reducer:profile-hydrate",initializer:r,after:"store"})},{"../../index":301,"../../reducers/profile":355,"../../services/store":376}],336:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("profile",i["default"],{})}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/profile"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:profile",initializer:r,before:"store"})},{"../../index":301,"../../reducers/profile":355,"../../services/store":376}],337:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("search",i["default"],Object.assign({},s.initialState,{providers:l["default"].get("SEARCH_PROVIDERS")||[],query:l["default"].get("SEARCH_QUERY")||""}))}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../.."),l=n(o),s=e("../../reducers/search"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:search",initializer:r,before:"store"})},{"../..":301,"../../reducers/search":356,"../../services/store":376}],338:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("selection",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/selection"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:selection",initializer:r,before:"store"})},{"../../index":301,"../../reducers/selection":357,"../../services/store":376}],339:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("snackbar",i["default"],s.initialState)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/snackbar"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:snackbar",initializer:r,before:"store"})},{"../../index":301,"../../reducers/snackbar":358,"../../services/store":376}],340:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=null;e=l["default"].has("THREAD")?(0,s.hydrate)(l["default"].get("THREAD")):{isBusy:!1},c["default"].addReducer("thread",i["default"],e)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/thread"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:thread",initializer:r,before:"store"})},{"../../index":301,"../../reducers/thread":359,"../../services/store":376}],341:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("threads",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/threads"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:threads",initializer:r,before:"store"})},{"../../index":301,"../../reducers/threads":360,"../../services/store":376}],342:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("tick",i["default"],s.initialState)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/tick"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:tick",initializer:r,before:"store"})},{"../../index":301,"../../reducers/tick":361,"../../services/store":376}],343:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("username-history",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/username-history"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:username-history",initializer:r,before:"store"})},{"../../index":301,"../../reducers/username-history":362,"../../services/store":376}],344:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].addReducer("users",i["default"],[])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../../index"),l=n(o),s=e("../../reducers/users"),i=n(s),u=e("../../services/store"),c=n(u);l["default"].addInitializer({name:"reducer:users",initializer:r,before:"store"})},{"../../index":301,"../../reducers/users":363,"../../services/store":376}],345:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init(c["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/snackbar"),i=n(s),u=e("../services/store"),c=n(u);l["default"].addInitializer({name:"snackbar",initializer:r,after:"store"})},{"../index":301,"../services/snackbar":375,"../services/store":376}],346:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){i["default"].init()}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/store"),i=n(s);l["default"].addInitializer({name:"store",initializer:r,before:"_end"})},{"../index":301,"../services/store":376}],347:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){window.setInterval(function(){u["default"].dispatch((0,s.doTick)())},c)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../reducers/tick"),i=e("../services/store"),u=n(i),c=5e4;l["default"].addInitializer({name:"tick-start",initializer:r,after:"store"})},{"../index":301,"../reducers/tick":361,"../services/store":376}],348:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(){c["default"].init(i["default"])}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=r;var o=e("../index"),l=n(o),s=e("../services/include"),i=n(s),u=e("../services/zxcvbn"),c=n(u);l["default"].addInitializer({name:"zxcvbn",initializer:r})},{"../index":301,"../services/include":367,"../services/zxcvbn":377}],349:[function(e,t,a){"use strict";function n(e){return{type:u,patch:e}}function r(e){return{type:c,user:e}}function o(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return{type:d,soft:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:i,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case u:var a=Object.assign({},e);return a.user=Object.assign({},e.user,t.patch),a;case c:return Object.assign({},e,{signedIn:t.user});case d:return Object.assign({},e,{isAuthenticated:!1,isAnonymous:!0,signedOut:!t.soft});case s.UPDATE_AVATAR:if(e.isAuthenticated&&e.user.id===t.userId){var n=Object.assign({},e);return n.user=Object.assign({},e.user,{avatars:t.avatars}),n}return e;case s.UPDATE_USERNAME:if(e.isAuthenticated&&e.user.id===t.userId){var r=Object.assign({},e);return r.user=Object.assign({},e.user,{username:t.username,slug:t.slug}),r}return e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.SIGN_OUT=a.SIGN_IN=a.PATCH_USER=a.initialState=void 0,a.patch=n,a.signIn=r,a.signOut=o,a["default"]=l;var s=e("./users"),i=a.initialState={signedIn:!1,signedOut:!1},u=a.PATCH_USER="PATCH_USER",c=a.SIGN_IN="SIGN_IN",d=a.SIGN_OUT="SIGN_OUT"},{"./users":363}],350:[function(e,t,a){"use strict";function n(e){return{type:o,state:e}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case o:return t.state;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.replace=n,a["default"]=r;var o=a.REPLACE_PARTICIPANTS="REPLACE_PARTICIPANTS"},{}],351:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=!1;for(var a in e.choices){var n=e.choices[a];if(n.selected){t=!0;break}}return Object.assign({},e,{posted_on:(0,f["default"])(e.posted_on),hasSelectedChoices:t,endsOn:e.length?(0,f["default"])(e.posted_on).add(e.length,"days"):null,isBusy:!1})}function o(){return{type:p}}function l(){return{type:m}}function s(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:b,state:t?e:r(e)}}function i(e){return{type:v,data:e}}function u(){return{type:h}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case p:return Object.assign({},e,{isBusy:!0});case m:return Object.assign({},e,{isBusy:!1});case h:return{isBusy:!1};case b:return t.state;case v:return Object.assign({},e,t.data);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_POLL=a.REPLACE_POLL=a.REMOVE_POLL=a.RELEASE_POLL=a.BUSY_POLL=void 0,a.hydrate=r,a.busy=o,a.release=l,a.replace=s,a.update=i,a.remove=u,a["default"]=c;var d=e("moment"),f=n(d),p=a.BUSY_POLL="BUSY_POLL",m=a.RELEASE_POLL="RELEASE_POLL",h=a.REMOVE_POLL="REMOVE_POLL",b=a.REPLACE_POLL="REPLACE_POLL",v=a.UPDATE_POLL="UPDATE_POLL"},{moment:"moment"}],352:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{posted_on:(0,u["default"])(e.posted_on),updated_on:(0,u["default"])(e.updated_on),hidden_on:(0,u["default"])(e.hidden_on),attachments:e.attachments?e.attachments.map(o):null,poster:e.poster?(0,c.hydrateUser)(e.poster):null,isSelected:!1,isBusy:!1,isDeleted:!1})}function o(e){return Object.assign({},e,{uploaded_on:(0,u["default"])(e.uploaded_on)})}function l(e,t){return{type:d,post:e,patch:t}}function s(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case d:return e.id==t.post.id?Object.assign({},e,t.patch):e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PATCH_POST=void 0,a.hydrate=r,a.hydrateAttachment=o,a.patch=l,a["default"]=s;var i=e("moment"),u=n(i),c=e("./users"),d=a.PATCH_POST="PATCH_POST"},{"./users":363,moment:"moment"}],353:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:b,post:e}}function o(e){return{type:v,post:e}}function l(){return{type:g}}function s(e){return Object.assign({},e,{results:e.results.map(p.hydrate),isLoaded:!0,isBusy:!1,isSelected:!1})}function i(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:_,state:t?e:s(e)}}function u(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:h,state:t?e:s(e)}}function c(){return{type:y}}function d(e){return{type:E,update:e}}function f(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case b:var a=e.results.map(function(e){return e.id==t.post.id?Object.assign({},e,{isSelected:!0}):e});return Object.assign({},e,{results:a});case v:var n=e.results.map(function(e){return e.id==t.post.id?Object.assign({},e,{isSelected:!1}):e});return Object.assign({},e,{results:n});case g:var r=e.results.map(function(e){return Object.assign({},e,{isSelected:!1})});return Object.assign({},e,{results:r});case h:var o=e.results.slice(),l=e.results.map(function(e){return e.id});return t.state.results.map(function(e){l.indexOf(e.id)===-1&&o.push(e)}),Object.assign({},t.state,{results:o});case _:return t.state;case y:return Object.assign({},e,{isLoaded:!1});case E:return Object.assign({},e,t.update);case p.PATCH_POST:var s=e.results.map(function(e){return(0,m["default"])(e,t)});return Object.assign({},e,{results:s});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_POSTS=a.UNLOAD_POSTS=a.LOAD_POSTS=a.DESELECT_POSTS=a.DESELECT_POST=a.SELECT_POST=a.APPEND_POSTS=void 0,a.select=r,a.deselect=o,a.deselectAll=l,a.hydrate=s,a.load=i,a.append=u,a.unload=c,a.update=d,a["default"]=f;var p=e("./post"),m=n(p),h=a.APPEND_POSTS="APPEND_POSTS",b=a.SELECT_POST="SELECT_POST",v=a.DESELECT_POST="DESELECT_POST",g=a.DESELECT_POSTS="DESELECT_POSTS",_=a.LOAD_POSTS="LOAD_POSTS",y=a.UNLOAD_POSTS="UNLOAD_POSTS",E=a.UPDATE_POSTS="UPDATE_POSTS"},{"./post":352}],354:[function(e,t,a){"use strict";function n(e){return{type:o,newState:e}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case o:return t.newState;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.load=n,a["default"]=r;var o=a.LOAD_DETAILS="LOAD_DETAILS"},{}],355:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:c,profile:e}}function o(e){return{type:d,patch:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case c:return Object.assign({},t.profile,{joined_on:(0,i["default"])(t.profile.joined_on),status:(0,u.hydrateStatus)(t.profile.status)});case d:return Object.assign({},e,t.patch);case u.UPDATE_AVATAR:return e.id===t.userId?Object.assign({},e,{
+avatars:t.avatars}):e;case u.UPDATE_USERNAME:return e.id===t.userId?Object.assign({},e,{username:t.username,slug:t.slug}):e;default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.PATCH_PROFILE=a.HYDRATE_PROFILE=void 0,a.hydrate=r,a.patch=o,a["default"]=l;var s=e("moment"),i=n(s),u=e("./users"),c=a.HYDRATE_PROFILE="HYDRATE_PROFILE",d=a.PATCH_PROFILE="PATCH_PROFILE"},{"./users":363,moment:"moment"}],356:[function(e,t,a){"use strict";function n(e){return{type:s,state:{isLoading:!1,providers:e}}}function r(e){return{type:i,update:e}}function o(e){return{type:u,provider:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case s:return t.state;case i:return Object.assign({},e,t.update);case u:return Object.assign({},e,{providers:e.providers.map(function(e){return e.id===t.provider.id?t.provider:e})});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.replace=n,a.update=r,a.updateProvider=o,a["default"]=l;var s=a.REPLACE_SEARCH="REPLACE_SEARCH",i=a.UPDATE_SEARCH="UPDATE_SEARCH",u=a.UPDATE_SEARCH_PROVIDER="UPDATE_SEARCH_PROVIDER";a.initialState={isLoading:!1,query:"",providers:[]}},{}],357:[function(e,t,a){"use strict";function n(e){return{type:i,items:e}}function r(){return{type:u}}function o(e){return{type:c,item:e}}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case i:return t.items;case u:return[];case c:return(0,s.toggle)(e,t.item);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.SELECT_ITEM=a.SELECT_NONE=a.SELECT_ALL=void 0,a.all=n,a.none=r,a.item=o,a["default"]=l;var s=e("../utils/sets"),i=a.SELECT_ALL="SELECT_ALL",u=a.SELECT_NONE="SELECT_NONE",c=a.SELECT_ITEM="SELECT_ITEM"},{"../utils/sets":390}],358:[function(e,t,a){"use strict";function n(e,t){return{type:s,message:e,messageType:t}}function r(){return{type:i}}function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:l,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return t.type===s?{type:t.messageType,message:t.message,isVisible:!0}:t.type===i?Object.assign({},e,{isVisible:!1}):e}Object.defineProperty(a,"__esModule",{value:!0}),a.showSnackbar=n,a.hideSnackbar=r,a["default"]=o;var l=a.initialState={type:"info",message:"",isVisible:!1},s=a.SHOW_SNACKBAR="SHOW_SNACKBAR",i=a.HIDE_SNACKBAR="HIDE_SNACKBAR"},{}],359:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return Object.assign({},e,{started_on:(0,f["default"])(e.started_on),last_post_on:(0,f["default"])(e.last_post_on),best_answer_marked_on:e.best_answer_marked_on?(0,f["default"])(e.best_answer_marked_on):null,isBusy:!1})}function o(){return{type:m}}function l(){return{type:h}}function s(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return{type:b,state:t?e:r(e)}}function i(e){return{type:v,data:e}}function u(e){return{type:g,data:e}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case m:return Object.assign({},e,{isBusy:!0});case h:return Object.assign({},e,{isBusy:!1});case p.REMOVE_POLL:return Object.assign({},e,{poll:null});case p.REPLACE_POLL:return Object.assign({},e,{poll:t.state});case b:return t.state;case v:return Object.assign({},e,t.data);case g:var a=Object.assign({},e.acl,t.data);return Object.assign({},e,{acl:a});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_THREAD_ACL=a.UPDATE_THREAD=a.REPLACE_THREAD=a.RELEASE_THREAD=a.BUSY_THREAD=void 0,a.hydrate=r,a.busy=o,a.release=l,a.replace=s,a.update=i,a.updateAcl=u,a["default"]=c;var d=e("moment"),f=n(d),p=e("./poll"),m=a.BUSY_THREAD="BUSY_THREAD",h=a.RELEASE_THREAD="RELEASE_THREAD",b=a.REPLACE_THREAD="REPLACE_THREAD",v=a.UPDATE_THREAD="UPDATE_THREAD",g=a.UPDATE_THREAD_ACL="UPDATE_THREAD_ACL"},{"./poll":351,moment:"moment"}],360:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){return{type:v,items:e,sorting:t}}function o(e){return{type:g,thread:e}}function l(e,t){return{type:_,category:e,categoriesMap:t}}function s(e){return{type:y,items:e}}function i(e,t){var a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;return{type:E,thread:e,patch:t,sorting:a}}function u(e){return{type:w,sorting:e}}function c(e){var t=[];return O.forEach(function(a){e[a]&&t.push(a)}),t}function d(e){return Object.assign({},e,{started_on:(0,m["default"])(e.started_on),last_post_on:(0,m["default"])(e.last_post_on),moderation:c(e.acl)})}function f(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case v:var a=(0,b["default"])(t.items.map(d),e);return a.sort(t.sorting);case g:return e.filter(function(e){return e.id!==t.thread.id});case _:return e.filter(function(e){var a=t.categoriesMap[e.category];return a.lft>=t.category.lft&&a.rght<=t.category.rght||2==e.weight});case y:return t.items.map(d);case E:var n=e.map(function(e){return e.id===t.thread.id?Object.assign({},e,t.patch):e});return t.sorting?n.sort(t.sorting):n;case w:return e.sort(t.sorting);default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.MODERATION_PERMISSIONS=a.SORT_THREADS=a.PATCH_THREAD=a.HYDRATE_THREADS=a.FILTER_THREADS=a.DELETE_THREAD=a.APPEND_THREADS=void 0,a.append=r,a.deleteThread=o,a.filterThreads=l,a.hydrate=s,a.patch=i,a.sort=u,a.getThreadModerationOptions=c,a.hydrateThread=d,a["default"]=f;var p=e("moment"),m=n(p),h=e("../utils/concat-unique"),b=n(h),v=a.APPEND_THREADS="APPEND_THREADS",g=a.DELETE_THREAD="DELETE_THREAD",_=a.FILTER_THREADS="FILTER_THREADS",y=a.HYDRATE_THREADS="HYDRATE_THREADS",E=a.PATCH_THREAD="PATCH_THREAD",w=a.SORT_THREADS="SORT_THREADS",O=a.MODERATION_PERMISSIONS=["can_announce","can_approve","can_close","can_hide","can_move","can_merge","can_pin","can_review"]},{"../utils/concat-unique":380,moment:"moment"}],361:[function(e,t,a){"use strict";function n(){return{type:l}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return t.type===l?Object.assign({},e,{tick:e.tick+1}):e}Object.defineProperty(a,"__esModule",{value:!0}),a.doTick=n,a["default"]=r;var o=a.initialState={tick:0},l=a.TICK="TICK"},{}],362:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,a){return{type:m,change:e,user:t,changedBy:a}}function o(e){return{type:h,items:e}}function l(e){return{type:b,items:e}}function s(e){return Object.assign({},e,{changed_on:(0,c["default"])(e.changed_on)})}function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case m:var a=e.slice();return a.unshift({id:Math.floor(Date.now()/1e3),changed_by:t.changedBy,changed_by_username:t.changedBy.username,changed_on:(0,c["default"])(),new_username:t.change.username,old_username:t.user.username}),a;case h:return(0,p["default"])(e,t.items.map(s));case b:return t.items.map(s);case d.UPDATE_AVATAR:return e.map(function(e){return e=Object.assign({},e),e.changed_by&&e.changed_by.id===t.userId&&(e.changed_by=Object.assign({},e.changed_by,{avatars:t.avatars})),e});case d.UPDATE_USERNAME:return e.map(function(e){return e=Object.assign({},e),e.changed_by&&e.changed_by.id===t.userId&&(e.changed_by=Object.assign({},e.changed_by,{username:t.username,slug:t.slug})),Object.assign({},e)});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.HYDRATE_HISTORY=a.APPEND_HISTORY=a.ADD_NAME_CHANGE=void 0,a.addNameChange=r,a.append=o,a.hydrate=l,a.hydrateNamechange=s,a["default"]=i;var u=e("moment"),c=n(u),d=e("./users"),f=e("../utils/concat-unique"),p=n(f),m=a.ADD_NAME_CHANGE="ADD_NAME_CHANGE",h=a.APPEND_HISTORY="APPEND_HISTORY",b=a.HYDRATE_HISTORY="HYDRATE_HISTORY"},{"../utils/concat-unique":380,"./users":363,moment:"moment"}],363:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:h,items:e}}function o(e){return{type:b,items:e}}function l(e){return e?Object.assign({},e,{last_click:e.last_click?(0,f["default"])(e.last_click):null,banned_until:e.banned_until?(0,f["default"])(e.banned_until):null}):null}function s(e){return Object.assign({},e,{joined_on:(0,f["default"])(e.joined_on),status:l(e.status)})}function i(e,t){return{type:v,userId:e.id,avatars:t}}function u(e,t,a){return{type:g,userId:e.id,username:t,slug:a}}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;switch(t.type){case h:return(0,m["default"])(e,t.items.map(s));case b:return t.items.map(s);case v:return e.map(function(e){return e=Object.assign({},e),e.id===t.userId&&(e.avatars=t.avatars),e});default:return e}}Object.defineProperty(a,"__esModule",{value:!0}),a.UPDATE_USERNAME=a.UPDATE_AVATAR=a.HYDRATE_USERS=a.APPEND_USERS=void 0,a.append=r,a.hydrate=o,a.hydrateStatus=l,a.hydrateUser=s,a.updateAvatar=i,a.updateUsername=u,a["default"]=c;var d=e("moment"),f=n(d),p=e("../utils/concat-unique"),m=n(p),h=a.APPEND_USERS="APPEND_USERS",b=a.HYDRATE_USERS="HYDRATE_USERS",v=a.UPDATE_AVATAR="UPDATE_AVATAR",g=a.UPDATE_USERNAME="UPDATE_USERNAME"},{"../utils/concat-unique":380,moment:"moment"}],364:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Ajax=function(){function e(){n(this,e),this._cookieName=null,this._csrfToken=null,this._locks={}}return r(e,[{key:"init",value:function(e){this._cookieName=e}},{key:"getCsrfToken",value:function(){if(document.cookie.indexOf(this._cookieName)!==-1){var e=new RegExp(this._cookieName+"=([^;]*)"),t=document.cookie.match(e)[0];return t?t.split("=")[1]:null}return null}},{key:"request",value:function(e,t,a){var n=this;return new Promise(function(r,o){var l={url:t,method:e,headers:{"X-CSRFToken":n.getCsrfToken()},data:a?JSON.stringify(a):null,contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){r(e)},error:function(e){var t=e.responseJSON||{};t.status=e.status,0===t.status&&(t.detail=gettext("Lost connection with application.")),404===t.status&&(t.detail&&"NOT FOUND"!==t.detail||(t.detail=gettext("Action link is invalid."))),500!==t.status||t.detail||(t.detail=gettext("Unknown error has occured.")),t.statusText=e.statusText,o(t)}};$.ajax(l)})}},{key:"get",value:function(e,t,a){if(t&&(e+="?"+$.param(t)),a){var n=this;return this._locks[a]&&(this._locks[a].url=e),this._locks[a]&&this._locks[a].waiter?{then:function(){}}:this._locks[a]&&this._locks[a].wait?(this._locks[a].waiter=!0,new Promise(function(t,r){var o=function l(e){n._locks[a].wait?window.setTimeout(function(){l(e)},300):n._locks[a].url!==e?l(n._locks[a].url):(n._locks[a].waiter=!1,n.request("GET",n._locks[a].url).then(function(r){n._locks[a].url===e?t(r):(n._locks[a].waiter=!0,l(n._locks[a].url))},function(t){n._locks[a].url===e?r(t):(n._locks[a].waiter=!0,l(n._locks[a].url))}))};window.setTimeout(function(){o(e)},300)})):(this._locks[a]={url:e,wait:!0,waiter:!1},new Promise(function(t,r){n.request("GET",e).then(function(r){n._locks[a].wait=!1,n._locks[a].url===e&&t(r)},function(t){n._locks[a].wait=!1,n._locks[a].url===e&&r(t)})}))}return this.request("GET",e)}},{key:"post",value:function(e,t){return this.request("POST",e,t)}},{key:"patch",value:function(e,t){return this.request("PATCH",e,t)}},{key:"put",value:function(e,t){return this.request("PUT",e,t)}},{key:"delete",value:function(e,t){return this.request("DELETE",e,t)}},{key:"upload",value:function(e,t,a){var n=this;return new Promise(function(r,o){var l={url:e,method:"POST",headers:{"X-CSRFToken":n.getCsrfToken()},data:t,contentType:!1,processData:!1,xhr:function s(){var s=new window.XMLHttpRequest;return s.upload.addEventListener("progress",function(e){e.lengthComputable&&a(Math.round(e.loaded/e.total*100))},!1),s},success:function(e){r(e)},error:function(e){var t=e.responseJSON||{};t.status=e.status,0===t.status&&(t.detail=gettext("Lost connection with application.")),413!==t.status||t.detail||(t.detail=gettext("Upload was rejected by server as too large.")),404===t.status&&(t.detail&&"NOT FOUND"!==t.detail||(t.detail=gettext("Action link is invalid."))),500!==t.status||t.detail||(t.detail=gettext("Unknown error has occured.")),t.statusText=e.statusText,o(t)}};$.ajax(l)})}}]),e}();a["default"]=new o},{}],365:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Auth=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("../reducers/auth"),l=a.Auth=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t,a){this._store=e,this._local=t,this._modal=a,this.syncSession(),this.watchState()}},{key:"syncSession",value:function(){var e=this._store.getState().auth;e.isAuthenticated?this._local.set("auth",{isAuthenticated:!0,username:e.user.username}):this._local.set("auth",{isAuthenticated:!1})}},{key:"watchState",value:function(){var e=this,t=this._store.getState().auth;this._local.watch("auth",function(a){a.isAuthenticated?e._store.dispatch((0,o.signIn)({username:a.username})):t.isAuthenticated&&e._store.dispatch((0,o.signOut)())}),this._modal.hide()}},{key:"signIn",value:function(e){this._store.dispatch((0,o.signIn)(e)),this._local.set("auth",{isAuthenticated:!0,username:e.username}),this._modal.hide()}},{key:"signOut",value:function(){this._store.dispatch((0,o.signOut)()),this._local.set("auth",{isAuthenticated:!1}),this._modal.hide()}},{key:"softSignOut",value:function(){this._store.dispatch((0,o.signOut)(!0)),this._local.set("auth",{isAuthenticated:!1}),this._modal.hide()}}]),e}();a["default"]=new l},{"../reducers/auth":349}],366:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Captcha=a.ReCaptcha=a.ReCaptchaComponent=a.QACaptcha=a.NoCaptcha=a.BaseCaptcha=void 0;var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),i=e("react"),u=n(i),c=e("../components/form-group"),d=n(c),f=a.BaseCaptcha=function(){function e(){l(this,e)}return s(e,[{key:"init",value:function(e,t,a,n){this._context=e,this._ajax=t,this._include=a,this._snackbar=n}}]),e}(),p=a.NoCaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){return new Promise(function(e){e()})}},{key:"validator",value:function(){return null}},{key:"component",value:function(){return null}}]),t}(f),m=a.QACaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){var e=this;return new Promise(function(t,a){e._ajax.get(e._context.get("CAPTCHA_API")).then(function(a){e.question=a.question,e.helpText=a.help_text,t()},function(){e._snackbar.error(gettext("Failed to load CAPTCHA.")),a()})})}},{key:"validator",value:function(){return[]}},{key:"component",value:function(e){return u["default"].createElement(d["default"],{label:this.question,"for":"id_captcha",labelClass:e.labelClass||"",controlClass:e.controlClass||"",validation:e.form.state.errors.captcha,helpText:this.helpText||null},u["default"].createElement("input",{"aria-describedby":"id_captcha_status",className:"form-control",disabled:e.form.state.isLoading,id:"id_captcha",onChange:e.form.bindInput("captcha"),type:"text",value:e.form.state.captcha}))}}]),t}(f),h=a.ReCaptchaComponent=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"componentDidMount",value:function(){var e=this;grecaptcha.render("recaptcha",{sitekey:this.props.siteKey,callback:function(t){e.props.binding({target:{value:t}})}})}},{key:"render",value:function(){return u["default"].createElement("div",{id:"recaptcha"})}}]),t}(u["default"].Component),b=a.ReCaptcha=function(e){function t(){return l(this,t),r(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return o(t,e),s(t,[{key:"load",value:function(){return this._include.include("https://www.google.com/recaptcha/api.js",!0),new Promise(function(e){var t=function a(){"undefined"==typeof grecaptcha?window.setTimeout(function(){a()},200):e()};t()})}},{key:"validator",value:function(){return[]}},{key:"component",value:function(e){return u["default"].createElement(d["default"],{label:gettext("Please solve the quick test"),"for":"id_captcha",labelClass:e.labelClass||"",controlClass:e.controlClass||"",validation:e.form.state.errors.captcha,helpText:gettext("This test helps us prevent automated spam registrations on our site.")},u["default"].createElement(h,{binding:e.form.bindInput("captcha"),siteKey:this._context.get("SETTINGS").recaptcha_site_key}))}}]),t}(f),v=a.Captcha=function(){function e(){l(this,e)}return s(e,[{key:"init",value:function(e,t,a,n){switch(e.get("SETTINGS").captcha_type){case"no":this._captcha=new p;break;case"qa":this._captcha=new m;break;case"re":this._captcha=new b}this._captcha.init(e,t,a,n)}},{key:"load",value:function(){return this._captcha.load()}},{key:"validator",value:function(){return this._captcha.validator()}},{key:"component",value:function(e){return this._captcha.component(e)}}]),e}();a["default"]=new v},{"../components/form-group":54,react:"react"}],367:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Include=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._staticUrl=e,this._included=[]}},{key:"include",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];this._included.indexOf(e)===-1&&(this._included.push(e),this._include(e,t))}},{key:"_include",value:function(e,t){$.ajax({url:(t?"":this._staticUrl)+e,cache:!0,dataType:"script"})}}]),e}();a["default"]=new o},{}],368:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=window.localStorage,l=a.LocalStorage=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){var t=this;this._prefix=e,this._watchers=[],window.addEventListener("storage",function(e){var a=JSON.parse(e.newValue);t._watchers.forEach(function(t){t.key===e.key&&e.oldValue!==e.newValue&&t.callback(a)})})}},{key:"set",value:function(e,t){o.setItem(this._prefix+e,JSON.stringify(t))}},{key:"get",value:function(e){var t=o.getItem(this._prefix+e);return t?JSON.parse(t):null}},{key:"watch",value:function(e,t){this._watchers.push({key:this._prefix+e,callback:t})}}]),e}();a["default"]=new l},{}],369:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.MobileNavbarDropdown=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("../utils/mount-component"),s=n(l),i=a.MobileNavbarDropdown=function(){function e(){r(this,e)}return o(e,[{key:"init",value:function(e){this._element=e,this._component=null}},{key:"show",value:function(e){this._component===e?this.hide():(this._component=e,(0,s["default"])(e,this._element.id),$(this._element).addClass("open"))}},{key:"showConnected",value:function(e,t){this._component===e?this.hide():(this._component=e,(0,s["default"])(t,this._element.id,!0),$(this._element).addClass("open"))}},{key:"hide",value:function(){$(this._element).removeClass("open"),this._component=null}}]),e}();a["default"]=new i},{"../utils/mount-component":385}],370:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Modal=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("react-dom"),s=n(l),i=e("../utils/mount-component"),u=n(i),c=a.Modal=function(){function e(){r(this,e)}return o(e,[{key:"init",value:function(e){var t=this;this._element=e,this._modal=$(e).modal({show:!1}),this._modal.on("hidden.bs.modal",function(){s["default"].unmountComponentAtNode(t._element)})}},{key:"show",value:function(e){(0,u["default"])(e,this._element.id),this._modal.modal("show")}},{key:"hide",value:function(){this._modal.modal("hide")}}]),e}();a["default"]=new c},{"../utils/mount-component":385,"react-dom":"react-dom"}],371:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e){var t=o(e),a=l(t);if(!a)return null;var n=0;if(t.indexOf("?")>0){var r=t.substr(t.indexOf("?")+1),s=r.split("&").filter(function(e){return"t="===e.substr(0,2)})[0];if(s){var i=s.substr(2).split("m");"s"===i[0].substr(-1)?n+=parseInt(i[0].substr(0,i[0].length-1)):(n+=60*parseInt(i[0]),i[1]&&"s"===i[1].substr(-1)&&(n+=parseInt(i[1].substr(0,i[1].length-1))))}}return{start:n,video:a}}function o(e){var t=e;return"https://"===e.substr(0,8)?t=t.substr(8):"http://"===e.substr(0,7)&&(t=t.substr(7)),"www."===t.substr(0,4)&&(t=t.substr(4)),t}function l(e){if(e.indexOf("youtu")===-1)return null;var t=e.match(i);return t?t[1]:null}Object.defineProperty(a,"__esModule",{value:!0});var s=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}();a.parseYoutubeUrl=r,a.cleanUrl=o,a.getVideoIdFromUrl=l;var i=new RegExp("^.*(?:(?:youtu.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)??v(?:i)?=|&v(?:i)?=))([^#&?]*).*"),u=a.OneBox=function(){function e(){var t=this;n(this,e),this.render=function(e){e&&(t.highlightCode(e),t.embedYoutubePlayers(e))},this._youtube={}}return s(e,[{key:"highlightCode",value:function(e){for(var t=e.querySelectorAll("pre>code"),a=0;a<t.length;a++){var n=t[a];hljs.highlightBlock(n)}}},{key:"embedYoutubePlayers",value:function(e){for(var t=e.querySelectorAll("p>a"),a=0;a<t.length;a++){var n=t[a],o=n.parentNode,l=1===o.childNodes.length;this._youtube[n.href]||(this._youtube[n.href]=r(n.href));var s=this._youtube[n.href];l&&s&&s.data!==!1&&this.swapYoutubePlayer(n,s)}}},{key:"swapYoutubePlayer",value:function(e,t){var a="https://www.youtube.com/embed/";a+=t.video,a+="?rel=0",t.start&&(a+="&start="+t.start);var n=$('<iframe class="embed-responsive-item" src="'+a+'" allowfullscreen></iframe>');$(e).replaceWith(n),n.wrap('<div class="embed-responsive embed-responsive-16by9"></div>')}}]),e}();a["default"]=new u},{}],372:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.PageTitle=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t){this._indexTitle=e,this._forumName=t}},{key:"set",value:function(e){if(!e)return void(document.title=this._indexTitle||this._forumName);"string"==typeof e&&(e={title:e});var t=e.title;if(e.page>1){var a=interpolate(gettext("page: %(page)s"),{page:e.page},!0);t+=" ("+a+")"}e.parent&&(t+=" | "+e.parent),document.title=t+" | "+this._forumName}}]),e}();a["default"]=new o},{}],373:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Polls=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e,t){this._ajax=e,this._snackbar=t,this._polls={}}},{key:"start",value:function(e){var t=this;this.stop(e.poll);var a=function n(){t._polls[e.poll]=e,t._ajax.get(e.url,e.data||null).then(function(a){t._polls[e.poll]._stopped||(e.update(a),t._polls[e.poll].timeout=window.setTimeout(n,e.frequency))},function(a){t._polls[e.poll]._stopped||(e.error?e.error(a):t._snackbar.apiError(a))})};e.delayed?this._polls[e.poll]={timeout:window.setTimeout(a,e.frequency)}:a()}},{key:"stop",value:function(e){this._polls[e]&&(window.clearTimeout(this._polls[e].timeout),this._polls[e]._stopped=!0)}}]),e}();a["default"]=new o},{}],374:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Posting=void 0;var o=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),l=e("react"),s=n(l),i=e("react-dom"),u=n(i),c=e("../components/poll"),d=e("../components/posting"),f=n(d),p=e("../utils/mount-component"),m=n(p),h=a.Posting=function(){function e(){var t=this;r(this,e),this.close=function(){t._isOpen&&!t._isClosing&&(t._isClosing=!0,t._placeholder.removeClass("slide-in"),window.setTimeout(function(){u["default"].unmountComponentAtNode(document.getElementById("posting-mount")),t._isClosing=!1,t._isOpen=!1},300))}}return o(e,[{key:"init",value:function(e,t,a){this._ajax=e,this._snackbar=t,this._placeholder=$(a),this._mode=null,this._isOpen=!1,this._isClosing=!1}},{key:"open",value:function(e){if(this._isOpen===!1)this._mode=e.mode,this._isOpen=e.submit,this._realOpen(e);else if(this._isOpen!==e.submit){var t=gettext("You are already working on other message. Do you want to discard it?");"POLL"==this._mode&&(t=gettext("You are already working on a poll. Do you want to discard it?"));var a=confirm(t);a&&(this._mode=e.mode,this._isOpen=e.submit,this._realOpen(e))}else"REPLY"==this._mode&&"REPLY"==e.mode&&this._realOpen(e)}},{key:"_realOpen",value:function(e){"POLL"==e.mode?(0,m["default"])(s["default"].createElement(c.PollForm,e),"posting-mount"):(0,m["default"])(s["default"].createElement(f["default"],e),"posting-mount"),this._placeholder.addClass("slide-in"),$("html, body").animate({scrollTop:this._placeholder.offset().top},1e3)}}]),e}();a["default"]=new h},{"../components/poll":105,"../components/posting":133,"../utils/mount-component":385,react:"react","react-dom":"react-dom"}],375:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.Snackbar=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("../reducers/snackbar"),l=300,s=5e3,i=a.Snackbar=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._store=e,this._timeout=null}},{key:"alert",value:function(e,t){var a=this;this._timeout?(window.clearTimeout(this._timeout),this._store.dispatch((0,o.hideSnackbar)()),this._timeout=window.setTimeout(function(){a._timeout=null,a.alert(e,t)},l)):(this._store.dispatch((0,o.showSnackbar)(e,t)),this._timeout=window.setTimeout(function(){a._store.dispatch((0,o.hideSnackbar)()),a._timeout=null},s))}},{key:"info",value:function(e){this.alert(e,"info")}},{key:"success",value:function(e){this.alert(e,"success")}},{key:"warning",value:function(e){this.alert(e,"warning")}},{key:"error",value:function(e){this.alert(e,"error")}},{key:"apiError",value:function(e){var t=e.detail;t||(t=404===e.status?gettext("Action link is invalid."):gettext("Unknown error has occured.")),403===e.status&&"Permission denied"===t&&(t=gettext("You don't have permission to perform this action.")),this.error(t)}}]),e}();a["default"]=new i},{"../reducers/snackbar":358}],376:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0}),a.StoreWrapper=void 0;var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=e("redux"),l=a.StoreWrapper=function(){function e(){n(this,e),this._store=null,this._reducers={},this._initialState={}}return r(e,[{key:"addReducer",value:function(e,t,a){this._reducers[e]=t,this._initialState[e]=a}},{key:"init",value:function(){this._store=(0,o.createStore)((0,o.combineReducers)(this._reducers),this._initialState)}},{key:"getStore",value:function(){return this._store}},{key:"getState",value:function(){return this._store.getState()}},{key:"dispatch",value:function(e){return this._store.dispatch(e)}}]),e}();a["default"]=new l},{redux:"redux"}],377:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){
+var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=a.Zxcvbn=function(){function e(){n(this,e)}return r(e,[{key:"init",value:function(e){this._include=e,this._isLoaded=!1}},{key:"scorePassword",value:function(e,t){return this._isLoaded?zxcvbn(e,t).score:0}},{key:"load",value:function(){return this._isLoaded?this._loadedPromise():(this._include.include("misago/js/zxcvbn.js"),this._loadingPromise())}},{key:"_loadingPromise",value:function(){var e=this;return new Promise(function(t,a){var n=function r(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;n+=1,n>200?a():"undefined"==typeof zxcvbn?window.setTimeout(function(){r(n)},200):(e._isLoaded=!0,t())};n()})}},{key:"_loadedPromise",value:function(){return new Promise(function(e){e()})}}]),e}();a["default"]=new o},{}],378:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){if(u["default"].render(s["default"].createElement(c.Provider,{store:b["default"].getStore()},s["default"].createElement(g,{message:e.message,expires:e.expires_on?(0,o["default"])(e.expires_on):null})),document.getElementById("page-mount")),"undefined"==typeof t||t){var a=m["default"].get("SETTINGS").forum_name;document.title=gettext("You are banned")+" | "+a,window.history.pushState({},"",m["default"].get("BANNED_URL"))}};var r=e("moment"),o=n(r),l=e("react"),s=n(l),i=e("react-dom"),u=n(i),c=e("react-redux"),d=e("../components/banned-page"),f=n(d),p=e("../index"),m=n(p),h=e("../services/store"),b=n(h),v=function(e){return e.tick},g=(0,c.connect)(v)(f["default"])},{"../components/banned-page":7,"../index":301,"../services/store":376,moment:"moment",react:"react","react-dom":"react-dom","react-redux":"react-redux"}],379:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=[],r=[];if(e.forEach(function(e){r.push(e),r.length===t&&(n.push(r),r=[])}),a!==!1&&r.length>0&&r.length<t)for(var o=r.length;o<t;o++)r.push(a);return r.length&&n.push(r),n}},{}],380:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=[];return e.concat(t).filter(function(e){return a.indexOf(e.id)===-1&&(a.push(e.id),!0)})}},{}],381:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=function(){function e(t,a){n(this,e),this._callback=t,this._count=a}return r(e,[{key:"count",value:function(){this._count-=1,0===this._count&&this._callback()}}]),e}();a["default"]=o},{}],382:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e.replace(/[&<>"']/g,function(e){return n[e]})};var n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}},{}],383:[function(e,t,a){"use strict";function n(e){return e.toFixed(1)}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return e>1073741824?n(e/1073741824)+" GB":e>1048576?n(e/1048576)+" MB":e>1024?n(e/1024)+" KB":n(e)+" B"},a.roundSize=n},{}],384:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){return n.test($.trim(e))};var n=new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$","i")},{}],385:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){var a=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],n=document.getElementById(t),r=e.props?e:o["default"].createElement(e,null);n&&(a?s["default"].render(o["default"].createElement(i.Provider,{store:c["default"].getStore()},r),n):s["default"].render(r,n))};var r=e("react"),o=n(r),l=e("react-dom"),s=n(l),i=e("react-redux"),u=e("../services/store"),c=n(u)},{"../services/store":376,react:"react","react-dom":"react-dom","react-redux":"react-redux"}],386:[function(e,t,a){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(a,"__esModule",{value:!0});var r=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,a,n){return a&&e(t.prototype,a),n&&e(t,n),t}}(),o=function(){function e(t){n(this,e),this.isOrdered=!1,this._items=t||[]}return r(e,[{key:"add",value:function(e,t,a){this._items.push({key:e,item:t,after:a?a.after||null:null,before:a?a.before||null:null})}},{key:"get",value:function(e,t){for(var a=0;a<this._items.length;a++)if(this._items[a].key===e)return this._items[a].item;return t}},{key:"has",value:function(e){return void 0!==this.get(e)}},{key:"values",value:function t(){for(var t=[],e=0;e<this._items.length;e++)t.push(this._items[e].item);return t}},{key:"order",value:function(e){return this.isOrdered||(this._items=this._order(this._items),this.isOrdered=!0),e||"undefined"==typeof e?this.values():this._items}},{key:"orderedValues",value:function(){return this.order(!0)}},{key:"_order",value:function(e){function t(e){var t=-1;r.indexOf(e.key)===-1&&(e.after?(t=r.indexOf(e.after),t!==-1&&(t+=1)):e.before&&(t=r.indexOf(e.before)),t!==-1&&(n.splice(t,0,e),r.splice(t,0,e.key)))}var a=[];e.forEach(function(e){a.push(e.key)});var n=[],r=[];e.forEach(function(e){e.after||e.before||(n.push(e),r.push(e.key))}),e.forEach(function(e){"_end"===e.before&&(n.push(e),r.push(e.key))});for(var o=200;o>0&&a.length!==r.length;)o-=1,e.forEach(t);return n}}]),e}();a["default"]=o},{}],387:[function(e,t,a){"use strict";function n(e,t){return Math.floor(Math.random()*(t-e+1))+e}function r(e,t){for(var a=new Array(n(e,t)),r=0;r<a.length;r++)a[r]=r;return a}Object.defineProperty(a,"__esModule",{value:!0}),a["int"]=n,a.range=r},{}],388:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(){window.scrollTo(0,0)}},{}],389:[function(e,t,a){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e){var t={component:e.component||null,childRoutes:[]};e.root?t.childRoutes=[{path:e.root,onEnter:function(t,a){a(null,e.paths[0].path)}}].concat(e.paths):t.childRoutes=e.paths,s["default"].render(o["default"].createElement(i.Provider,{store:d["default"].getStore()},o["default"].createElement(u.Router,{routes:t,history:u.browserHistory})),f)};var r=e("react"),o=n(r),l=e("react-dom"),s=n(l),i=e("react-redux"),u=e("react-router"),c=e("../services/store"),d=n(c),f=document.getElementById("page-mount")},{"../services/store":376,react:"react","react-dom":"react-dom","react-redux":"react-redux","react-router":"react-router"}],390:[function(e,t,a){"use strict";function n(e,t){if(e.indexOf(t)===-1){var a=e.slice();return a.push(t),a}return e}function r(e,t){return e.indexOf(t)>=0?e.filter(function(e){return e!==t}):e}function o(e,t){if(e.indexOf(t)===-1){var a=e.slice();return a.push(t),a}return e.filter(function(e){return e!==t})}Object.defineProperty(a,"__esModule",{value:!0}),a.push=n,a.remove=r,a.toggle=o},{}],391:[function(e,t,a){"use strict";Object.defineProperty(a,"__esModule",{value:!0}),a["default"]=function(e,t){if(e=(e+"").toLowerCase(),t=(t+"").toLowerCase(),t.length<=0)return 0;for(var a=0,n=0,r=t.length;;){if(n=e.indexOf(t,n),!(n>=0))break;a+=1,n+=r}return a}},{}],392:[function(e,t,a){"use strict";function n(e){return function(t){if(t===!1||null===t||0===$.trim(t).length)return e||gettext("This field is required.")}}function r(e){var t=gettext("You have to accept the terms of service.");return n(e||t)}function o(e){var t=gettext("You have to accept the privacy policy.");return n(e||t)}function l(e){return function(t){if(!p.test(t))return e||gettext("Enter a valid email address.")}}function s(e,t){return function(a){var n="",r=$.trim(a).length;if(r<e)return n=t?t(e,r):ngettext("Ensure this value has at least %(limit_value)s character (it has %(show_value)s).","Ensure this value has at least %(limit_value)s characters (it has %(show_value)s).",e),interpolate(n,{limit_value:e,show_value:r},!0)}}function i(e,t){return function(a){var n="",r=$.trim(a).length;if(r>e)return n=t?t(e,r):ngettext("Ensure this value has at most %(limit_value)s character (it has %(show_value)s).","Ensure this value has at most %(limit_value)s characters (it has %(show_value)s).",e),interpolate(n,{limit_value:e,show_value:r},!0)}}function u(e){var t=function(e){return ngettext("Username must be at least %(limit_value)s character long.","Username must be at least %(limit_value)s characters long.",e)};return s(e,t)}function c(e){var t=function(e){return ngettext("Username cannot be longer than %(limit_value)s character.","Username cannot be longer than %(limit_value)s characters.",e)};return i(e,t)}function d(){return function(e){if(!m.test($.trim(e)))return gettext("Username can only contain latin alphabet letters and digits.")}}function f(e){return function(t){var a=t.length;if(a<e){var n=ngettext("Valid password must be at least %(limit_value)s character long.","Valid password must be at least %(limit_value)s characters long.",e);return interpolate(n,{limit_value:e,show_value:a},!0)}}}Object.defineProperty(a,"__esModule",{value:!0}),a.required=n,a.requiredTermsOfService=r,a.requiredPrivacyPolicy=o,a.email=l,a.minLength=s,a.maxLength=i,a.usernameMinLength=u,a.usernameMaxLength=c,a.usernameContent=d,a.passwordMinLength=f;var p=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i,m=new RegExp("^[0-9a-z]+$","i")},{}]},{},[301,302,303,304,305,322,323,324,325,326,327,328,329,345,346,347,348,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344]);
 //# sourceMappingURL=misago.js.map

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
misago/static/misago/js/misago.js.map


+ 68 - 0
misago/templates/misago/admin/agreements/form.html

@@ -0,0 +1,68 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_forms %}
+
+
+{% block title %}
+{% if target.pk %}
+{% trans target.get_final_title %}
+{% else %}
+{% trans "New agreement" %}
+{% endif %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% if target.pk %}
+{% trans target.get_final_title %}
+{% else %}
+{% trans "New agreement" %}
+{% endif %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% if target.pk %}
+  {% trans target.get_final_title %}
+  {% else %}
+  {% trans "New agreement" %}
+  {% endif %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+
+{% block form-body %}
+<div class="form-body">
+  {% with label_class="col-md-3" field_class="col-md-9" %}
+  <fieldset>
+    <legend>{% trans "Basic settings" %}</legend>
+
+    {% form_row form.type label_class field_class %}
+    {% form_row form.title label_class field_class %}
+    {% form_row form.is_active "col-md-offset-3" field_class %}
+
+  </fieldset>
+  <fieldset>
+    <legend>{% trans "Agreement contents" %}</legend>
+
+    <div class="form-group">
+      <p>{% trans "Fill in one of the fields." %}</p>
+    </div>
+
+    {% form_row form.link label_class field_class %}
+    {% form_row form.text label_class field_class %}
+
+  </fieldset>
+  {% endwith %}
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}

+ 148 - 0
misago/templates/misago/admin/agreements/list.html

@@ -0,0 +1,148 @@
+{% extends "misago/admin/generic/list.html" %}
+{% load i18n misago_capture misago_forms %}
+
+
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:users:agreements:new' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "New agreement" %}
+  </a>
+</div>
+{% endblock %}
+
+
+{% block table-header %}
+<th>{% trans "Title" %}</th>
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 180px;">{% trans "Type" %}</th>
+<th style="width: 250px;">{% trans "Created" %}</th>
+<th style="width: 250px;">{% trans "Modified" %}</th>
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 1%;">&nbsp;</th>
+{% endblock table-header %}
+
+
+{% block table-row %}
+<td class="item-name">
+  {{ item.get_final_title }}
+</td>
+<td class="lead text-muted">
+  {% if item.is_active %}
+    <span class="fa fa-check-square tooltip-top" title="{% blocktrans trimmed with type=item.get_type_display.lower %}Active {{ type }}{% endblocktrans %}"></span>
+  {% else %}
+    &nbsp;
+  {% endif %}
+</td>
+<td>
+  {{ item.get_type_display }}
+</td>
+<td>
+  {% capture trimmed as created_on %}
+    <abbr class="moment" data-iso="{{ item.created_on.isoformat }}" data-format="LL"></abbr>
+  {% endcapture %}
+  {% capture trimmed as created_by %}
+    {% if item.created_by %}
+      <a href="{{ item.created_by.get_absolute_url }}" class="item-title">{{ item.created_by }}</a>
+    {% else %}
+      <span class="item-title">{{ item.created_by_name }}</span>
+    {% endif %}
+  {% endcapture %}
+  {% blocktrans trimmed with created_on=created_on|safe created_by=created_by|safe %}
+    {{ created_on }} by {{ created_by }}
+  {% endblocktrans %}
+</td>
+<td>
+  {% if item.last_modified_on %}
+    {% capture trimmed as last_modified_on %}
+      <abbr class="moment" data-iso="{{ item.last_modified_on.isoformat }}" data-format="LL"></abbr>
+    {% endcapture %}
+    {% capture trimmed as last_modified_by %}
+      {% if item.last_modified_by %}
+        <a href="{{ item.last_modified_by.get_absolute_url }}" class="item-title">{{ item.last_modified_by }}</a>
+      {% else %}
+        <span class="item-title">{{ item.last_modified_by }}</span>
+      {% endif %}
+    {% endcapture %}
+    {% blocktrans trimmed with last_modified_on=last_modified_on|safe last_modified_by=last_modified_by|safe %}
+      {{ last_modified_on }} by {{ last_modified_by }}
+    {% endblocktrans %}
+  {% else %}
+    <em>{% trans "never" %}</em>
+  {% endif %}
+</td>
+<td class="row-action">
+  {% if not item.is_active %}
+    <form action="{% url 'misago:admin:users:agreements:set-as-active' pk=item.pk %}" method="post" class="set-as-active-prompt">
+      <button class="btn btn-primary tooltip-top" title="{% trans "Set as active" %}">
+        {% csrf_token %}
+        <span class="fa fa-check-square"></span>
+      </button>
+    </form>
+  {% else %}
+    &nbsp;
+  {% endif %}
+</td>
+<td class="row-action">
+  <a href="{% url 'misago:admin:users:agreements:edit' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
+    <span class="fa fa-pencil"></span>
+  </a>
+</td>
+<td class="row-action">
+  <form action="{% url 'misago:admin:users:agreements:delete' pk=item.pk %}" method="post" class="delete-prompt">
+    <button class="btn btn-danger tooltip-top" title="{% trans "Remove" %}">
+      {% csrf_token %}
+      <span class="fa fa-times"></span>
+    </button>
+  </form>
+</td>
+{% endblock %}
+
+
+{% block emptylist %}
+<td colspan="9">
+  {% if active_filters %}
+  <p>{% trans "No agreements matching search criteria have been found" %}</p>
+  {% else %}
+  <p>{% trans "No agreements are currently set." %}</p>
+  {% endif %}
+</td>
+{% endblock emptylist %}
+
+
+{% block javascripts %}
+{{ block.super }}
+<script type="text/javascript">
+  $(function() {
+    $('.set-as-active-prompt').submit(function() {
+      var decision = confirm("{% trans 'Are you sure you want to set this agreement as active for its type?' %}");
+      return decision;
+    });
+
+    $('.delete-prompt').submit(function() {
+      var decision = confirm("{% trans 'Are you sure you want to delete this agreement?' %}");
+      return decision;
+    });
+  });
+</script>
+{% endblock %}
+
+
+{% block modal-title %}
+{% trans "Search bans" %}
+{% endblock modal-title %}
+
+
+{% block modal-body %}
+<div class="row">
+  <div class="col-md-12">
+    {% form_row search_form.type %}
+  </div>
+</div>
+<div class="row">
+  <div class="col-md-12">
+    {% form_row search_form.content %}
+  </div>
+</div>
+{% endblock modal-body %}

+ 2 - 2
misago/templates/misago/admin/attachments/list.html

@@ -39,8 +39,8 @@
     <abbr class="moment" data-iso="{{ item.uploaded_on.isoformat }}" data-format="LL"></abbr>
   {% endcapture %}
   <small class="text-muted">
-    {% blocktrans trimmed with filetype=item.filetype size=item.size|filesizeformat uploader=uploader|safe uploaded_on=uploaded_on|safe uploader_ip=item.uploader_ip %}
-      {{ filetype }}, {{ size }}, uploaded by {{ uploader }} {{ uploaded_on }} from {{ uploader_ip }}.
+    {% blocktrans trimmed with filetype=item.filetype size=item.size|filesizeformat uploader=uploader|safe uploaded_on=uploaded_on|safe %}
+      {{ filetype }}, {{ size }}, uploaded by {{ uploader }} {{ uploaded_on }}.
     {% endblocktrans %}
   </small>
 </td>

+ 38 - 0
misago/templates/misago/admin/datadownloads/form.html

@@ -0,0 +1,38 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_forms %}
+
+
+{% block title %}
+{% trans "Request new data downloads" %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% trans "Request new data downloads" %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% trans "Request new data downloads" %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+
+{% block form-body %}
+<div class="form-body">
+  <fieldset>
+    {% form_row form.user_identifiers %}
+  </fieldset>
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}

+ 113 - 0
misago/templates/misago/admin/datadownloads/list.html

@@ -0,0 +1,113 @@
+{% extends "misago/admin/generic/list.html" %}
+{% load i18n misago_avatars misago_forms %}
+
+
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:users:data-downloads:request' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "Request new downloads" %}
+  </a>
+</div>
+{% endblock %}
+
+
+{% block table-header %}
+<th style="width: 1%;">&nbsp;</th>
+<th>{% trans "User" %}</th>
+<th>{% trans "Status" %}</th>
+<th>{% trans "Requested on" %}</th>
+<th style="width: 1%;">&nbsp;</th>
+<th>{% trans "Requested by" %}</th>
+{% for action in extra_actions %}
+<th style="width: 1%;">&nbsp;</th>
+{% endfor %}
+<th style="width: 1%;">&nbsp;</th>
+{% endblock table-header %}
+
+
+{% block table-row %}
+<td>
+  <a href="{{ item.user.get_absolute_url }}">
+    <img src="{{ item.user|avatar:24 }}" alt="{% trans "Avatar" %}" width="24" height="24">
+  </a>
+</td>
+<td class="item-name">
+  <a href="{{ item.user.get_absolute_url }}">
+    {{ item.user }}
+  </a>
+</td>
+<td>
+  {{ item.get_status_display }}
+</td>
+<td>
+  <abbr class="moment" data-iso="{{ item.requested_on.isoformat }}" data-format="LL"></abbr>
+</td>
+<td>
+  {% if item.requester %}
+    <a href="{{ item.requester.get_absolute_url }}">
+      <img src="{{ item.requester|avatar:24 }}" alt="{% trans "Avatar" %}" width="24" height="24">
+    </a>
+  {% else %}
+    <img src="{{ BLANK_AVATAR_URL }}" alt="{% trans "Avatar" %}" width="24" height="24">
+  {% endif %}
+</td>
+<td class="item-name">
+  {% if item.requester %}
+    <a href="{{ item.requester.get_absolute_url }}">
+      {{ item.requester }}
+    </a>
+  {% else %}
+    {{ item.requester_name }}
+  {% endif %}
+</td>
+{% for action in extra_actions %}
+<td class="row-action">
+  <a href="{% url action.link pk=item.pk %}" class="btn btn-{% if action.style %}{{ action.style }}{% else %}default{% endif %} tooltip-top" title="{{ action.name }}">
+    <span class="{{ action.icon }}"></span>
+  </a>
+</td>
+{% endfor %}
+<td class="row-action">
+  {% if item.file %}
+    <a href="{{ item.file.url }}" class="btn btn-primary tooltip-top" title="{% trans "Download data" %}">
+      <span class="fa fa-download"></span>
+    </a>
+  {% endif %}
+</td>
+{% endblock table-row %}
+
+
+{% block emptylist %}
+<td colspan="{{ 7|add:extra_actions_len }}">
+  {% if active_filters %}
+    <p>{% trans "No data downloads matching search criteria have been found." %}</p>
+  {% else %}
+    <p>{% trans "No data downloads exist at the moment." %}</p>
+  {% endif %}
+</td>
+{% endblock emptylist %}
+
+
+{% block modal-title %}
+{% trans "Search data downloads" %}
+{% endblock modal-title %}
+
+
+{% block modal-body %}
+<div class="row">
+  <div class="col-md-12">
+    {% form_row search_form.status %}
+  </div>
+</div>
+<div class="row">
+  <div class="col-md-12">
+    {% form_row search_form.user %}
+  </div>
+</div>
+<div class="row">
+  <div class="col-md-12">
+    {% form_row search_form.requested_by %}
+  </div>
+</div>
+{% endblock modal-body %}

+ 29 - 2
misago/templates/misago/admin/index.html

@@ -1,5 +1,5 @@
 {% extends "misago/admin/base.html" %}
-{% load i18n %}
+{% load i18n misago_capture %}
 
 
 {% block title %}{% trans "Home" %} | {{ block.super }}{% endblock %}
@@ -19,7 +19,34 @@
   <div class="row">
     <div class="col-md-8">
 
-
+      <h2>{% trans "System checks" %}</h2>
+
+      {% if address_check.is_correct %}
+        <div class="alert alert-success" role="alert">
+          <p>{% trans "MISAGO_ADDRESS setting appears to be correct." %}</p>
+        </div>
+      {% elif address_check.set_address %}
+        <div class="alert alert-warning" role="alert">
+          <p class="lead">{% trans "The settings.py value for MISAGO_ADDRESS appears to be incorrect." %}</p>
+          <p>
+            {% capture trimmed as set_address %}
+              <code>{{ address_check.set_address }}</code>
+            {% endcapture %}
+            {% capture trimmed as correct_address %}
+              <code>{{ address_check.correct_address }}</code>
+            {% endcapture %}
+            {% blocktrans trimmed with configured_address=set_address|safe correct_address=correct_address|safe %}
+              Your MISAGO_ADDRESS is set to {{ configured_address }} while correct value appears to be {{ correct_address }}.
+            {% endblocktrans %}
+          </p>
+          <p>{% trans "Misago uses this setting to build correct links in e-mails sent to site users." %}</p>
+        </div>
+      {% else %}
+        <div class="alert alert-danger" role="alert">
+          <p class="lead">{% trans "The settings.py is missing MISAGO_ADDRESS value." %}</p>
+          <p>{% trans "Misago uses this setting to build correct links in e-mails sent to site users." %}</p>
+        </div>
+      {% endif %}
 
     </div>
     <div class="col-md-4">

+ 5 - 1
misago/templates/misago/admin/users/ban.html

@@ -42,7 +42,11 @@ class="form-horizontal"
       {{ user.email }}
     </td>
     <td>
-      {{ user.joined_from_ip }}
+      {% if user.joined_from_ip %}
+        {{ user.joined_from_ip }}
+      {% else %}
+        <i>{% trans "IP not available" %}</i>
+      {% endif %}
     </td>
   </tr>
   {% endfor %}

+ 31 - 0
misago/templates/misago/admin/users/edit.html

@@ -212,6 +212,37 @@ class="form-horizontal"
     {% endif %}
 
   </fieldset>
+  <fieldset>
+    <legend>{% trans "Agreements" %}</legend>
+    <table class="table table-condensed">
+      <thead>
+        <tr>
+          <th>{% trans "Agreement" %}</th>
+          <th style="width: 250px;">{% trans "Accepted on" %}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for agreement in target.useragreement_set.select_related.iterator %}
+          <tr>
+            <td>
+              <a href="{% url 'misago:admin:users:agreements:edit' pk=agreement.agreement_id %}">
+                {{ agreement.agreement.get_final_title }}
+              </a>
+            </td>
+            <td>
+              <abbr class="moment" data-iso="{{ agreement.accepted_on.isoformat }}" data-format="LL"></abbr>
+            </td>
+          </tr>
+        {% empty %}
+          <tr>
+            <td colspan="2">
+              {% trans "This user didn't accept any agreements." %}
+            </td>
+          </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+  </fieldset>
   {% endwith %}
 </div>
 {% endblock form-body %}

+ 8 - 0
misago/templates/misago/admin/users/list.html

@@ -18,6 +18,7 @@
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 200px;">{% trans "E-mail" %}</th>
+<th style="width: 128px;">{% trans "IP Address" %}</th>
 <th style="width: 150px;">{% trans "Rank" %}</th>
 <th style="width: 200px;">{% trans "Joined" %}</th>
 <th style="width: 128px;">{% trans "Posts" %}</th>
@@ -71,6 +72,13 @@
   <a href="mailto:{{ item.email }}">{{ item.email }}</a>
 </td>
 <td>
+  {% if item.joined_from_ip %}
+    {{ item.joined_from_ip }}
+  {% else %}
+    <i>{% trans "IP removed" %}</i>
+  {% endif %}
+</td>
+<td>
   {{ item.rank }}
 </td>
 <td>

+ 6 - 2
misago/templates/misago/base.html

@@ -21,12 +21,12 @@
     {% include "misago/head.html" %}
     <script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","url":"{{ SITE_ADDRESS }}"}</script>
   </head>
-  <body>
+  <body {% if misago_agreement %}class="agreement-overlay-visible"{% endif %}>
 
     <div id="auth-message-mount"></div>
     <div id="snackbar-mount"></div>
 
-    <div id="misago-container">
+    <div id="misago-container" {% if misago_agreement %}aria-hidden="true"{% endif %}>
       {% include "misago/jumbotron.html" %}
       {% include "misago/navbar.html" %}
 
@@ -41,6 +41,10 @@
 
     <div class="modal fade" id="modal-mount" tabindex="-1" role="dialog" aria-labelledby="misago-modal-label"></div>
 
+    {% if misago_agreement %}
+      {% include "misago/required_agreement.html" %}
+    {% endif %}
+
     <script src="{% url 'django-i18n' %}?{{ LANGUAGE_CODE }}"></script>
     <script type="text/javascript">
       if (!(Object.assign && Array.from)) {

+ 3 - 3
misago/templates/misago/emails/activation/by_admin.html

@@ -1,15 +1,15 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, your account has been activated by forum administrator.
 {% endblocktrans %}
 <br>
 <br>
 {% capture trimmed as login_link %}
-<a href="{{ SITE_ADDRESS }}{% url LOGIN_URL %}">{% trans "this form" %}</a>
+<a href="{% absoluteurl LOGIN_URL %}">{% trans "this form" %}</a>
 {% endcapture %}
 {% blocktrans trimmed with login_form=login_link|safe %}
 You can now sign in to it using {{ login_form }}.

+ 3 - 3
misago/templates/misago/emails/activation/by_admin.txt

@@ -1,14 +1,14 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, your account has been activated by forum administrator.
 {% endblocktrans %}
 
 {% blocktrans trimmed %}
 You can now sign in to it using the form below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url LOGIN_URL %}
+{% absoluteurl LOGIN_URL %}
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/activation/by_user.html

@@ -1,13 +1,13 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, to activate your account click the below link:
 {% endblocktrans %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{% url 'misago:activate-by-token' pk=recipient.pk token=activation_token %}">{% trans "Activate my account!" %}</a>
+<a href="{% absoluteurl 'misago:activate-by-token' pk=user.pk token=activation_token %}">{% trans "Activate my account!" %}</a>
 <br>
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/activation/by_user.txt

@@ -1,10 +1,10 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, to activate your account click the below link:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url 'misago:activate-by-token' pk=recipient.pk token=activation_token %}
+{% absoluteurl 'misago:activate-by-token' pk=user.pk token=activation_token %}
 {% endblock content %}

+ 2 - 1
misago/templates/misago/emails/base.html

@@ -1,3 +1,4 @@
+{% load misago_absoluteurl %}
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 <html lang="{{ LANGUAGE_CODE }}">
 <head>
@@ -55,7 +56,7 @@
               <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0">
                 <tr>
                   <td valign="middle" style="font-size: 28px; line-height: 24px; color: #555555;">{{ misago_settings.forum_name }}</td>
-                  <td align="center" valign="middle" width="30"><img src="{{ SITE_ADDRESS }}{% url 'misago:user-avatar' pk=recipient.pk size=32 %}" width="32" height="32" style="border-radius: 3px;" alt=""></td>
+                  <td align="center" valign="middle" width="30"><img src="{% absoluteurl 'misago:user-avatar' pk=user.pk size=32 %}" width="32" height="32" style="border-radius: 3px;" alt=""></td>
                 </tr>
               </table>
             <br>

+ 3 - 3
misago/templates/misago/emails/change_email.html

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you have changed your e-mail address.
 {% endblocktrans %}
 <br>
@@ -13,6 +13,6 @@ To confirm this change, click the link below:
 {% endblocktrans %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{% url 'misago:options-confirm-email-change' token=token %}">{% trans "Save changes" %}</a>
+<a href="{% absoluteurl 'misago:options-confirm-email-change' token=token %}">{% trans "Save changes" %}</a>
 <br>
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/change_email.txt

@@ -1,14 +1,14 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you have changed your e-mail address.
 {% endblocktrans %}
 
 {% blocktrans trimmed %}
 To confirm this change, click the link below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url 'misago:options-confirm-email-change' token=token %}
+{% absoluteurl 'misago:options-confirm-email-change' token=token %}
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/change_password.html

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you have changed your password.
 {% endblocktrans %}
 <br>
@@ -13,6 +13,6 @@ To confirm this change, click the link below:
 {% endblocktrans %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{% url 'misago:options-confirm-password-change' token=token %}">{% trans "Save changes" %}</a>
+<a href="{% absoluteurl 'misago:options-confirm-password-change' token=token %}">{% trans "Save changes" %}</a>
 <br>
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/change_password.txt

@@ -1,14 +1,14 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you have changed your password.
 {% endblocktrans %}
 
 {% blocktrans trimmed %}
 To confirm this change, click the link below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url 'misago:options-confirm-password-change' token=token %}
+{% absoluteurl 'misago:options-confirm-password-change' token=token %}
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/change_password_form_link.html

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you want to change forgotten password for your forum account.
 {% endblocktrans %}
 <br>
@@ -13,6 +13,6 @@ To change your account password click the link below:
 {% endblocktrans %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{% url 'misago:forgotten-password-change-form' pk=recipient.pk token=confirmation_token %}">{% trans "Set new password" %}</a>
+<a href="{% absoluteurl 'misago:forgotten-password-change-form' pk=user.pk token=confirmation_token %}">{% trans "Set new password" %}</a>
 <br>
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/change_password_form_link.txt

@@ -1,14 +1,14 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, you are receiving this message because you want to change forgotten password for your forum account.
 {% endblocktrans %}
 
 {% blocktrans trimmed %}
 To change your account password click the link below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url 'misago:forgotten-password-change-form' pk=recipient.pk token=confirmation_token %}
+{% absoluteurl 'misago:forgotten-password-change-form' pk=user.pk token=confirmation_token %}
 {% endblock content %}

+ 20 - 0
misago/templates/misago/emails/data_download.html

@@ -0,0 +1,20 @@
+{% extends "misago/emails/base.html" %}
+{% load i18n misago_absoluteurl %}
+
+
+{% block content %}
+{% blocktrans trimmed with user=user %}
+{{ user }}, you are receiving this message because your data is ready for download.
+{% endblocktrans %}
+<br>
+<br>
+<a href="{% absoluteurl data_download.file.url %}">{% trans "Download data" %}</a>
+<br>
+<br>
+{% blocktrans trimmed count expires_in=expires_in %}
+This link will remain active for {{ expires_in }} hour from the time this message has been sent.
+{% plural %}
+This link will remain active for {{ expires_in }} hours from the time this message has been sent.
+{% endblocktrans %}
+<br>
+{% endblock content %}

+ 20 - 0
misago/templates/misago/emails/data_download.txt

@@ -0,0 +1,20 @@
+{% extends "misago/emails/base.txt" %}
+{% load i18n misago_absoluteurl %}
+
+
+{% block content %}
+{% blocktrans trimmed with user=user %}
+{{ user }}, you are receiving this message because your data is ready for download.
+{% endblocktrans %}
+
+{% blocktrans trimmed %}
+To download your data, click the following link:
+{% endblocktrans %}
+{% absoluteurl data_download.file.url %}
+
+{% blocktrans trimmed count expires_in=expires_in %}
+This link will remain active for {{ expires_in }} hour from the time this message has been sent.
+{% plural %}
+This link will remain active for {{ expires_in }} hours from the time this message has been sent.
+{% endblocktrans %}
+{% endblock content %}

+ 4 - 4
misago/templates/misago/emails/privatethread/added.html

@@ -1,12 +1,12 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block content %}
 {% capture trimmed as thread_title %}
-<strong>{{ thread.title }}</strong>
+<b>{{ thread.title }}</b>
 {% endcapture %}
-{% blocktrans trimmed with user=recipient.username sender=sender.username thread=thread_title %}
+{% blocktrans trimmed with user=user.username sender=sender.username thread=thread_title|safe %}
 {{ user }}, you are receiving this message because {{ sender }} has invited you to participate in private thread {{ thread }}.
 {% endblocktrans %}
 <br>
@@ -16,6 +16,6 @@ To read this thread click the link below:
 {% endblocktrans %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{{ thread.get_absolute_url }}">{{ thread.title }}</a>
+<a href="{% absoluteurl thread.get_absolute_url %}">{{ thread.title }}</a>
 <br>
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/privatethread/added.txt

@@ -1,14 +1,14 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient.username sender=sender.username thread=thread.title %}
+{% blocktrans trimmed with user=user.username sender=sender.username thread=thread.title %}
 {{ user }}, you are receiving this message because {{ sender }} has invited you to participate in private thread "{{ thread }}".
 {% endblocktrans %}
 
 {% blocktrans trimmed %}
 To read this thread click the link below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{{ thread.get_absolute_url }}
+{% absoluteurl thread.get_absolute_url %}
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/register/complete.html

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, thank you for joining us!
 {% endblocktrans %}
 <br>
@@ -15,7 +15,7 @@ You may now join discussion on our forums. Why not spend a minute or two to have
 <br>
 <br>
 {% capture trimmed as login_link %}
-<a href="{{ SITE_ADDRESS }}{% url LOGIN_URL %}">{% trans "this form" %}</a>
+<a href="{% absoluteurl LOGIN_URL %}">{% trans "this form" %}</a>
 {% endcapture %}
 {% blocktrans trimmed with login_form=login_link|safe %}
 You can always sign in to your account using {{ login_form }}.

+ 3 - 3
misago/templates/misago/emails/register/complete.txt

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, thank you for joining us!
 {% endblocktrans %}
 
@@ -14,5 +14,5 @@ You may now join discussion on our forums. Why not spend a minute or two to have
 {% blocktrans trimmed %}
 You can always sign in to your account using the form below:
 {% endblocktrans %}
-{{ SITE_ADDRESS }}{% url LOGIN_URL %}
+{% absoluteurl LOGIN_URL %}
 {% endblock content %}

+ 3 - 3
misago/templates/misago/emails/register/inactive.html

@@ -1,5 +1,5 @@
 {% extends "misago/emails/register/complete.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block activation-message %}
@@ -24,11 +24,11 @@
   {% endblocktrans %}
   <br>
   <br>
-  <a href="{{ SITE_ADDRESS }}{% url 'misago:activate-by-token' pk=recipient.pk token=activation_token %}">{% trans "Activate my account!" %}</a>
+  <a href="{% absoluteurl 'misago:activate-by-token' pk=user.pk token=activation_token %}">{% trans "Activate my account!" %}</a>
   <br>
   <br>
   {% capture trimmed as login_link %}
-  <a href="{{ SITE_ADDRESS }}{% url LOGIN_URL %}">{% trans "this form" %}</a>
+  <a href="{% absoluteurl LOGIN_URL %}">{% trans "this form" %}</a>
   {% endcapture %}
   {% blocktrans trimmed with login_form=login_link|safe %}
   Once your account is activated, you can always sign in to it using {{ login_form }}.

+ 4 - 4
misago/templates/misago/emails/register/inactive.txt

@@ -1,9 +1,9 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient %}
+{% blocktrans trimmed with user=user %}
 {{ user }}, thank you for joining us!
 {% endblocktrans %}
 {% if activation_by_admin %}
@@ -25,11 +25,11 @@
   {% blocktrans trimmed %}
   Before you will be able to join discussion on our forums, you have to activate your account. To do so, simply click the link below:
   {% endblocktrans %}
-  {{ SITE_ADDRESS }}{% url 'misago:activate-by-token' pk=recipient.pk token=activation_token %}"
+  {% absoluteurl 'misago:activate-by-token' pk=user.pk token=activation_token %}"
 
   {% blocktrans trimmed with login_form=login_link|safe %}
   Once your account is activated, you can always sign in to it using the form below:
   {% endblocktrans %}
-  {{ SITE_ADDRESS }}{% url LOGIN_URL %}
+  {% absoluteurl LOGIN_URL %}
 {% endif %}
 {% endblock content %}

+ 5 - 5
misago/templates/misago/emails/thread/reply.html

@@ -1,19 +1,19 @@
 {% extends "misago/emails/base.html" %}
-{% load i18n misago_capture %}
+{% load i18n misago_absoluteurl misago_capture %}
 
 
 {% block content %}
 {% capture trimmed as thread_link %}
-  <a href="{{ SITE_ADDRESS }}{{ thread.get_absolute_url }}">{{ thread }}</a>
+  <a href="{% absoluteurl thread.get_absolute_url %}">{{ thread }}</a>
 {% endcapture %}
-{% blocktrans trimmed with user=recipient poster=user thread=thread_link|safe %}
-{{ user }}, you are receiving this message because {{ poster }} has replied to the thread {{ thread }} that you are subscribed to.
+{% blocktrans trimmed with user=user sender=sender thread=thread_link|safe %}
+{{ user }}, you are receiving this message because {{ sender }} has replied to the thread {{ thread }} that you are subscribed to.
 {% endblocktrans %}
 <br>
 <br>
 {% trans "To read this reply, click the below link:" %}
 <br>
 <br>
-<a href="{{ SITE_ADDRESS }}{{ post.get_absolute_url }}">{% trans "Go to reply" %}</a>
+<a href="{% absoluteurl post.get_absolute_url %}">{% trans "Go to reply" %}</a>
 <br>
 {% endblock content %}

+ 4 - 4
misago/templates/misago/emails/thread/reply.txt

@@ -1,12 +1,12 @@
 {% extends "misago/emails/base.txt" %}
-{% load i18n %}
+{% load i18n misago_absoluteurl %}
 
 
 {% block content %}
-{% blocktrans trimmed with user=recipient poster=user thread=thread %}
-{{ user }}, you are receiving this message because {{ poster }} has replied to the thread "{{ thread }}" that you are subscribed to.
+{% blocktrans trimmed with user=user sender=sender thread=thread %}
+{{ user }}, you are receiving this message because {{ sender }} has replied to the thread "{{ thread }}" that you are subscribed to.
 {% endblocktrans %}
 
 {% trans "To read this reply, click the below link:" %}
-{{ SITE_ADDRESS }}{{ post.get_absolute_url }}
+{% absoluteurl post.get_absolute_url %}
 {% endblock content %}

+ 17 - 17
misago/templates/misago/errorpages/ban_message.html

@@ -1,21 +1,21 @@
 {% load i18n %}
-{% with ban.get_serialized_message.detail as ban_detail %}
-  {% if ban_detail.html %}
-    <div class="lead">
-      {{ ban_detail.html|safe }}
-    </div>
-  {% else %}
-    <p class="lead">
-      {{ ban_detail.plain }}
-    </p>
-  {% endif %}
+{% with ban.get_serialized_message.message as ban_message %}
+    {% if ban_message.html %}
+        <div class="lead">
+            {{ ban_message.html|safe }}
+        </div>
+    {% else %}
+        <p class="lead">
+            {{ ban_message.plain }}
+        </p>
+    {% endif %}
 {% endwith %}
 <p className="message-footnote">
-  {% if ban.expires_on %}
-    {% blocktrans trimmed with expires_on=ban.expires_on|date:"DATETIME_FORMAT" %}
-      This ban expires on {{ expires_on }}.
-    {% endblocktrans %}
-  {% else %}
-    {% trans "This ban is permanent." %}
-  {% endif %}
+    {% if ban.expires_on %}
+        {% blocktrans trimmed with expires_on=ban.expires_on|date:"DATETIME_FORMAT" %}
+            This ban expires on {{ expires_on }}.
+        {% endblocktrans %}
+    {% else %}
+        {% trans "This ban is permanent." %}
+    {% endif %}
 </p>

+ 5 - 5
misago/templates/misago/footer.html

@@ -10,21 +10,21 @@
         </p>
       </noscript>
 
-      {% if misago_settings.terms_of_service or misago_settings.terms_of_service_link or misago_settings.privacy_policy or misago_settings.privacy_policy_link or misago_settings.forum_footnote %}
+      {% if TERMS_OF_SERVICE_URL or PRIVACY_POLICY_URL or misago_settings.forum_footnote %}
         <ul class="list-inline footer-nav">
         {% if misago_settings.forum_footnote %}
           <li class="site-footnote">
             {{ misago_settings.forum_footnote }}
           </li>
         {% endif %}
-        {% if misago_settings.terms_of_service or misago_settings.terms_of_service_link %}
+        {% if TERMS_OF_SERVICE_URL %}
           <li>
-            <a href="{% url 'misago:terms-of-service' %}">{% trans "Terms of service" %}</a>
+            <a href="{{ TERMS_OF_SERVICE_URL }}">{% trans "Terms of service" %}</a>
           </li>
         {% endif %}
-        {% if misago_settings.privacy_policy or misago_settings.privacy_policy_link %}
+        {% if PRIVACY_POLICY_URL %}
           <li>
-            <a href="{% url 'misago:privacy-policy' %}">{% trans "Privacy policy" %}</a>
+            <a href="{{ PRIVACY_POLICY_URL }}">{% trans "Privacy policy" %}</a>
           </li>
         {% endif %}
         </ul>

+ 1 - 1
misago/templates/misago/privacy_policy.html

@@ -23,7 +23,7 @@
 
   <div class="legal-body container">
     <article class="misago-markup">
-      {{ body|safe }}
+      {{ text|safe }}
     </article>
   </div>
 </div>

+ 35 - 0
misago/templates/misago/required_agreement.html

@@ -0,0 +1,35 @@
+{% load i18n misago_capture %}
+<div class="agreement-overlay">
+  <div class="container">
+    <div class="agreement-content">
+      <div class="agreement-header">
+        <h2>{{ misago_agreement.title }}</h2>
+        {% if not misago_agreement.link %}
+          <p class="lead">
+            {% blocktrans trimmed with agreement=misago_agreement.type %}
+              Please review the updated {{ agreement }}:
+            {% endblocktrans %}
+          </p>
+        {% endif %}
+      </div>
+      <div class="agreement-body">
+        {% if misago_agreement.link %}
+          <p class="lead">
+            {% capture trimmed as link %}
+              <a href="{{ misago_agreement.link }}" target="_blank">{% trans "here" %}</a>
+            {% endcapture %}
+            {% blocktrans trimmed with agreement=misago_agreement.type link=link|safe %}
+              Please review the updated {{ agreement }} available {{ link }}.
+            {% endblocktrans %}
+          </p>
+        {% else %}
+          <article class="misago-markup">
+            {{ misago_agreement.text|safe }}
+          </article>
+        {% endif %}
+      </div>
+      <div id="required-agreement-mount" class="agreement-footer">
+      </div>
+    </div>
+  </div>
+</div>

+ 1 - 1
misago/templates/misago/terms_of_service.html

@@ -23,7 +23,7 @@
 
   <div class="legal-body container">
     <article class="misago-markup">
-      {{ body|safe }}
+      {{ text|safe }}
     </article>
   </div>
 </div>

+ 0 - 5
misago/templates/misago/thread/posts/event/info.html

@@ -29,9 +29,4 @@
       By {{ event_by }} on {{ event_on }}.
     {% endblocktrans %}
   </li>
-  {% if user.acl_cache.can_see_users_ips %}
-    <li class="event-ip">
-      <abbr title="{{ post.poster_ip }}">{% trans "IP recorded" %}</abbr>
-    </li>
-  {% endif %}
 </ul>

+ 96 - 5
misago/threads/api/attachments.py

@@ -1,11 +1,14 @@
 from rest_framework import viewsets
 from rest_framework.response import Response
 
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
+from django.template.defaultfilters import filesizeformat
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.threads.serializers import AttachmentSerializer, NewAttachmentSerializer
+from misago.threads.models import Attachment, AttachmentType
+from misago.threads.serializers import AttachmentSerializer
+from misago.users.audittrail import create_audit_trail
 
 
 IMAGE_EXTENSIONS = ('jpg', 'jpeg', 'png', 'gif')
@@ -16,8 +19,96 @@ class AttachmentViewSet(viewsets.ViewSet):
         if not request.user.acl_cache['max_attachment_size']:
             raise PermissionDenied(_("You don't have permission to upload new files."))
 
-        serializer = NewAttachmentSerializer(data=request.data, context={'request': request})
-        serializer.is_valid(raise_exception=True)
-        attachment = serializer.save()
+        try:
+            return self.create_attachment(request)
+        except ValidationError as e:
+            return Response({'detail': e.args[0]}, status=400)
+
+    def create_attachment(self, request):
+        upload = request.FILES.get('upload')
+        if not upload:
+            raise ValidationError(_("No file has been uploaded."))
+
+        user_roles = set(r.pk for r in request.user.get_roles())
+        filetype = validate_filetype(upload, user_roles)
+        validate_filesize(upload, filetype, request.user.acl_cache['max_attachment_size'])
+
+        attachment = Attachment(
+            secret=Attachment.generate_new_secret(),
+            filetype=filetype,
+            size=upload.size,
+            uploader=request.user,
+            uploader_name=request.user.username,
+            uploader_slug=request.user.slug,
+            filename=upload.name,
+        )
+
+        if is_upload_image(upload):
+            try:
+                attachment.set_image(upload)
+            except IOError:
+                raise ValidationError(_("Uploaded image was corrupted or invalid."))
+        else:
+            attachment.set_file(upload)
+
+        attachment.save()
         add_acl(request.user, attachment)
+
+        create_audit_trail(request, attachment)
+
         return Response(AttachmentSerializer(attachment, context={'user': request.user}).data)
+
+
+def validate_filetype(upload, user_roles):
+    filename = upload.name.strip().lower()
+
+    queryset = AttachmentType.objects.filter(status=AttachmentType.ENABLED)
+    for filetype in queryset.prefetch_related('limit_uploads_to'):
+        for extension in filetype.extensions_list:
+            if filename.endswith('.%s' % extension):
+                break
+        else:
+            continue
+
+        if filetype.mimetypes_list and upload.content_type not in filetype.mimetypes_list:
+            continue
+
+        if filetype.limit_uploads_to.exists():
+            allowed_roles = set(r.pk for r in filetype.limit_uploads_to.all())
+            if not user_roles & allowed_roles:
+                continue
+
+        return filetype
+
+    raise ValidationError(_("You can't upload files of this type."))
+
+
+def validate_filesize(upload, filetype, hard_limit):
+    if upload.size > hard_limit * 1024:
+        message = _("You can't upload files larger than %(limit)s (your file has %(upload)s).")
+        raise ValidationError(
+            message % {
+                'upload': filesizeformat(upload.size).rstrip('.0'),
+                'limit': filesizeformat(hard_limit * 1024).rstrip('.0'),
+            }
+        )
+
+    if filetype.size_limit and upload.size > filetype.size_limit * 1024:
+        message = _(
+            "You can't upload files of this type larger than %(limit)s (your file has %(upload)s)."
+        )
+        raise ValidationError(
+            message % {
+                'upload': filesizeformat(upload.size).rstrip('.0'),
+                'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0'),
+            }
+        )
+
+
+def is_upload_image(upload):
+    filename = upload.name.strip().lower()
+
+    for extension in IMAGE_EXTENSIONS:
+        if filename.endswith('.%s' % extension):
+            return True
+    return False

+ 13 - 12
misago/threads/api/pollvotecreateendpoint.py

@@ -13,20 +13,25 @@ def poll_vote_create(request, thread, poll):
     allow_vote_poll(request.user, poll)
 
     serializer = NewVoteSerializer(
-        # FIXME: lets use {'choices': []} JSON instead!
-        data={'choices': request.data},
+        data={
+            'choices': request.data,
+        },
         context={
             'allowed_choices': poll.allowed_choices,
             'choices': poll.choices,
         },
     )
 
-    serializer.is_valid(raise_exception=True)
-
-    validated_choices = serializer.validated_data['choices']
+    if not serializer.is_valid():
+        return Response(
+            {
+                'detail': serializer.errors['choices'][0],
+            },
+            status=400,
+        )
 
-    remove_user_votes(request.user, poll, validated_choices)
-    set_new_votes(request, poll, validated_choices)
+    remove_user_votes(request.user, poll, serializer.data['choices'])
+    set_new_votes(request, poll, serializer.data['choices'])
 
     add_acl(request.user, poll)
     serialized_poll = PollSerializer(poll).data
@@ -53,10 +58,7 @@ def remove_user_votes(user, poll, final_votes):
             removed_votes.append(choice['hash'])
 
     if removed_votes:
-        poll.pollvote_set.filter(
-            voter=user,
-            choice_hash__in=removed_votes,
-        ).delete()
+        poll.pollvote_set.filter(voter=user, choice_hash__in=removed_votes).delete()
 
 
 def set_new_votes(request, poll, final_votes):
@@ -73,5 +75,4 @@ def set_new_votes(request, poll, final_votes):
                 voter_name=request.user.username,
                 voter_slug=request.user.slug,
                 choice_hash=choice['hash'],
-                voter_ip=request.user_ip,
             )

+ 8 - 2
misago/threads/api/postendpoints/delete.py

@@ -5,7 +5,8 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
 from misago.conf import settings
-from misago.threads import moderation
+from misago.core.utils import clean_ids_list
+from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import (
     allow_delete_best_answer, allow_delete_event, allow_delete_post)
 from misago.threads.permissions import exclude_invisible_posts
@@ -37,7 +38,12 @@ def delete_bulk(request, thread):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        if 'posts' in serializer.errors:
+            errors = serializer.errors['posts']
+        else:
+            errors = list(serializer.errors.values())[0]
+        return Response({'detail': errors[0]}, status=400)
 
     for post in serializer.validated_data['posts']:
         post.delete()

+ 1 - 3
misago/threads/api/postendpoints/edits.py

@@ -2,7 +2,6 @@ from rest_framework.response import Response
 
 from django.core.exceptions import PermissionDenied
 from django.db.models import F
-from django.http import Http404
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils.translation import ugettext as _
@@ -48,7 +47,6 @@ def revert_post_endpoint(request, post):
         editor=request.user,
         editor_name=request.user.username,
         editor_slug=request.user.slug,
-        editor_ip=request.user_ip,
         edited_from=post.original,
         edited_to=edit.edited_from,
     )
@@ -87,7 +85,7 @@ def get_edit(post, pk=None):
 
     edit = post.edits_record.first()
     if not edit:
-        raise Http404()
+        raise PermissionDenied(_("Edits record is unavailable for this post."))
     return edit
 
 

+ 7 - 1
misago/threads/api/postendpoints/merge.py

@@ -19,7 +19,13 @@ def posts_merge_endpoint(request, thread):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        return Response(
+            {
+                'detail': list(serializer.errors.values())[0][0],
+            },
+            status=400,
+        )
 
     posts = serializer.validated_data['posts']
     first_post, merged_posts = posts[0], posts[1:]

+ 7 - 2
misago/threads/api/postendpoints/move.py

@@ -18,8 +18,13 @@ def posts_move_endpoint(request, thread, viewmodel):
             'viewmodel': viewmodel,
         }
     )
-    
-    serializer.is_valid(raise_exception=True)
+
+    if not serializer.is_valid():
+        if 'new_thread' in serializer.errors:
+            errors = serializer.errors['new_thread']
+        else:
+            errors = list(serializer.errors.values())[0]
+        return Response({'detail': errors[0]}, status=400)
 
     new_thread = serializer.validated_data['new_thread']
 

+ 2 - 2
misago/threads/api/postendpoints/patch_event.py

@@ -2,8 +2,8 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.api.patch import ApiPatch
-from misago.threads import moderation
+from misago.core.apipatch import ApiPatch
+from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import allow_hide_event, allow_unhide_event
 
 

+ 4 - 4
misago/threads/api/postendpoints/patch_post.py

@@ -5,10 +5,10 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.api.patch import ApiPatch
 from misago.conf import settings
+from misago.core.apipatch import ApiPatch
 from misago.threads.models import PostLike
-from misago.threads import moderation
+from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import (
     allow_approve_post, allow_hide_best_answer, allow_hide_post, allow_protect_post,
     allow_unhide_post)
@@ -61,7 +61,6 @@ def patch_is_liked(request, post, value):
             liker=request.user,
             liker_name=request.user.username,
             liker_slug=request.user.slug,
-            liker_ip=request.user_ip,
         )
         post.likes += 1
 
@@ -148,7 +147,8 @@ def post_patch_endpoint(request, post):
 
 def bulk_patch_endpoint(request, thread):
     serializer = BulkPatchSerializer(data=request.data)
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        return Response(serializer.errors, status=400)
 
     posts = clean_posts_for_patch(request, thread, serializer.data['ids'])
 

+ 10 - 2
misago/threads/api/postendpoints/split.py

@@ -4,8 +4,8 @@ from django.core.exceptions import PermissionDenied
 from django.utils import six
 from django.utils.translation import ugettext as _
 
-from misago.threads import moderation
 from misago.threads.models import Thread
+from misago.threads.moderation import threads as moderation
 from misago.threads.serializers import SplitPostsSerializer
 
 
@@ -21,7 +21,15 @@ def posts_split_endpoint(request, thread):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        if 'posts' in serializer.errors:
+            errors = {
+                'detail': serializer.errors['posts'][0]
+            }
+        else :
+            errors = serializer.errors
+
+        return Response(errors, status=400)
 
     split_posts_to_new_thread(request, thread, serializer.validated_data)
 

+ 3 - 16
misago/threads/api/postingendpoint/__init__.py

@@ -24,13 +24,12 @@ class PostingEndpoint(object):
         # we are using lock on user model to protect us from flood
         request.user.lock()
 
-        # store mode
-        self.mode = mode
-
         # build kwargs dict for passing to middlewares
         self.kwargs = kwargs
         self.kwargs.update({'mode': mode, 'request': request, 'user': request.user})
 
+        self.__dict__.update(kwargs)
+
         # some middlewares (eg. emailnotification) may call render()
         # which will crash if this isn't set to false
         request.include_frontend_context = False
@@ -135,19 +134,7 @@ class PostingMiddleware(object):
 
     def __init__(self, **kwargs):
         self.kwargs = kwargs
-
-        self.prefix = kwargs['prefix']
-
-        self.mode = kwargs['mode']
-        self.request = kwargs['request']
-        self.user = kwargs['user']
-
-        self.datetime = kwargs['datetime']
-        self.parsing_result = kwargs['parsing_result']
-        self.tree_name = kwargs.get('tree_name')
-
-        self.thread = kwargs['thread']
-        self.post = kwargs['post']
+        self.__dict__.update(kwargs)
 
     def use_this_middleware(self):
         return True

+ 1 - 1
misago/threads/api/postingendpoint/attachments.py

@@ -113,8 +113,8 @@ class AttachmentsSerializer(serializers.Serializer):
         if attachments:
             post.attachments_cache = AttachmentSerializer(attachments, many=True).data
             for attachment in post.attachments_cache:
+                del attachment['acl']
                 del attachment['post']
-                del attachment['uploader_ip']
         else:
             post.attachments_cache = None
         post.update_fields.append('attachments_cache')

+ 4 - 3
misago/threads/api/postingendpoint/category.py

@@ -5,7 +5,8 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext_lazy
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.categories.permissions import can_browse_category, can_see_category
 from misago.threads.permissions import allow_start_thread
 from misago.threads.threadtypes import trees_map
@@ -18,7 +19,7 @@ class CategoryMiddleware(PostingMiddleware):
 
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
-            return self.tree_name == THREADS_ROOT
+            return self.tree_name == THREADS_ROOT_NAME
         return False
 
     def get_serializer(self):
@@ -55,7 +56,7 @@ class CategorySerializer(serializers.Serializer):
     def validate_category(self, value):
         try:
             self.category_cache = Category.objects.get(
-                pk=value, tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT)
+                pk=value, tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
             )
 
             can_see = can_see_category(self.user, self.category_cache)

+ 2 - 2
misago/threads/api/postingendpoint/emailnotification.py

@@ -43,11 +43,11 @@ class EmailNotificationMiddleware(PostingMiddleware):
         subject_formats = {'user': self.user.username, 'thread': self.thread.title}
 
         return build_mail(
-            self.request,
             subscriber,
             subject % subject_formats,
             'misago/emails/thread/reply',
-            {
+            sender=self.user,
+            context={
                 'thread': self.thread,
                 'post': self.post,
             },

+ 2 - 2
misago/threads/api/postingendpoint/moderationqueue.py

@@ -1,4 +1,4 @@
-from misago.categories.models import PRIVATE_THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 
 from . import PostingEndpoint, PostingMiddleware
 
@@ -10,7 +10,7 @@ class ModerationQueueMiddleware(PostingMiddleware):
         except AttributeError:
             tree_name = self.thread.category.thread_type.root_name
 
-        return tree_name != PRIVATE_THREADS_ROOT
+        return tree_name != PRIVATE_THREADS_ROOT_NAME
 
     def save(self, serializer):
         if self.mode == PostingEndpoint.START:

+ 2 - 2
misago/threads/api/postingendpoint/participants.py

@@ -6,7 +6,7 @@ from django.utils import six
 from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
-from misago.categories.models import PRIVATE_THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 from misago.threads.participants import add_participants, set_owner
 from misago.threads.permissions import allow_message_user
 
@@ -19,7 +19,7 @@ UserModel = get_user_model()
 class ParticipantsMiddleware(PostingMiddleware):
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
-            return self.tree_name == PRIVATE_THREADS_ROOT
+            return self.tree_name == PRIVATE_THREADS_ROOT_NAME
         return False
 
     def get_serializer(self):

+ 3 - 2
misago/threads/api/postingendpoint/privatethread.py

@@ -1,5 +1,6 @@
 from misago.acl import add_acl
-from misago.categories.models import PRIVATE_THREADS_ROOT, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
+from misago.categories.models import Category
 
 from . import PostingEndpoint, PostingMiddleware
 
@@ -9,7 +10,7 @@ class PrivateThreadMiddleware(PostingMiddleware):
 
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
-            return self.tree_name == PRIVATE_THREADS_ROOT
+            return self.tree_name == PRIVATE_THREADS_ROOT_NAME
         return False
 
     def pre_save(self, serializer):

+ 0 - 1
misago/threads/api/postingendpoint/recordedit.py

@@ -32,7 +32,6 @@ class RecordEditMiddleware(PostingMiddleware):
             editor=self.user,
             editor_name=self.user.username,
             editor_slug=self.user.slug,
-            editor_ip=self.request.user_ip,
             edited_from=self.original_post,
             edited_to=self.post.original,
         )

+ 3 - 1
misago/threads/api/postingendpoint/reply.py

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy
 from misago.markup import common_flavour
 from misago.threads.checksums import update_post_checksum
 from misago.threads.validators import validate_post, validate_post_length, validate_title
+from misago.users.audittrail import create_audit_trail
 
 from . import PostingEndpoint, PostingMiddleware
 
@@ -46,6 +47,8 @@ class ReplyMiddleware(PostingMiddleware):
 
         self.thread.save()
 
+        create_audit_trail(self.request, self.post)
+
         # annotate post for future middlewares
         self.post.parsing_result = parsing_result
 
@@ -67,7 +70,6 @@ class ReplyMiddleware(PostingMiddleware):
         self.post.thread = self.thread
         self.post.poster = self.user
         self.post.poster_name = self.user.username
-        self.post.poster_ip = self.request.user_ip
         self.post.posted_on = self.datetime
 
         self.post.original = parsing_result['original_text']

+ 2 - 2
misago/threads/api/postingendpoint/syncprivatethreads.py

@@ -1,4 +1,4 @@
-from misago.categories.models import PRIVATE_THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 from misago.threads.participants import set_users_unread_private_threads_sync
 
 from . import PostingEndpoint, PostingMiddleware
@@ -9,7 +9,7 @@ class SyncPrivateThreadsMiddleware(PostingMiddleware):
 
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.REPLY:
-            return self.thread.thread_type.root_name == PRIVATE_THREADS_ROOT
+            return self.thread.thread_type.root_name == PRIVATE_THREADS_ROOT_NAME
         return False
 
     def post_save(self, serializer):

+ 2 - 2
misago/threads/api/postingendpoint/updatestats.py

@@ -1,6 +1,6 @@
 from django.db.models import F
 
-from misago.categories.models import THREADS_ROOT
+from misago.categories import THREADS_ROOT_NAME
 
 from . import PostingEndpoint, PostingMiddleware
 
@@ -41,7 +41,7 @@ class UpdateStatsMiddleware(PostingMiddleware):
         if post.is_unapproved:
             return  # don't update user on moderated post
 
-        if self.thread.thread_type.root_name == THREADS_ROOT:
+        if self.thread.thread_type.root_name == THREADS_ROOT_NAME:
             if self.mode == PostingEndpoint.START:
                 user.threads = F('threads') + 1
                 user.update_fields.append('threads')

+ 11 - 2
misago/threads/api/threadendpoints/delete.py

@@ -2,7 +2,7 @@ from rest_framework.response import Response
 
 from django.db import transaction
 
-from misago.threads import moderation
+from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import allow_delete_thread
 from misago.threads.serializers import DeleteThreadsSerializer
 
@@ -26,7 +26,16 @@ def delete_bulk(request, viewmodel):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        if 'threads' in serializer.errors:
+            errors = serializer.errors['threads']
+            if 'details' in errors:
+                return Response(
+                    hydrate_error_details(errors['details']), status=400)
+            return Response({'detail': errors[0]}, status=403)
+        else:
+            errors = list(serializer.errors)[0][0]
+            return Response({'detail': errors}, status=400)
 
     for thread in serializer.validated_data['threads']:
         with transaction.atomic():

+ 3 - 2
misago/threads/api/threadendpoints/editor.py

@@ -4,7 +4,8 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.threads.permissions import can_start_thread
 from misago.threads.threadtypes import trees_map
 
@@ -19,7 +20,7 @@ def thread_start_editor(request):
 
     queryset = Category.objects.filter(
         pk__in=request.user.acl_cache['browseable_categories'],
-        tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT)
+        tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
     ).order_by('-lft')
 
     for category in queryset:

+ 1 - 1
misago/threads/api/threadendpoints/list.py

@@ -18,7 +18,7 @@ class ThreadsList(object):
         category = self.get_category(request, pk=request.query_params.get('category'))
         threads = self.get_threads(request, category, list_type, page)
 
-        return Response(self.get_response_json(request, category, threads)['threads'])
+        return Response(self.get_response_json(request, category, threads)['THREADS'])
 
     def get_category(self, request, pk=None):
         raise NotImplementedError('Threads list has to implement get_category(request, pk=None)')

+ 59 - 17
misago/threads/api/threadendpoints/merge.py

@@ -6,9 +6,10 @@ from django.utils.six import text_type
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.threads import moderation
 from misago.threads.events import record_event
+from misago.threads.mergeconflict import MergeConflict
 from misago.threads.models import Thread
+from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import allow_merge_thread
 from misago.threads.serializers import (
     MergeThreadSerializer, MergeThreadsSerializer, ThreadsListSerializer)
@@ -26,7 +27,20 @@ def thread_merge_endpoint(request, thread, viewmodel):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        if 'other_thread' in serializer.errors:
+            errors = serializer.errors['other_thread']
+        elif 'best_answer' in serializer.errors:
+            errors = serializer.errors['best_answer']
+        elif 'best_answers' in serializer.errors:
+            return Response({'best_answers': serializer.errors['best_answers']}, status=400)
+        elif 'poll' in serializer.errors:
+            errors = serializer.errors['poll']
+        elif 'polls' in serializer.errors:
+            return Response({'polls': serializer.errors['polls']}, status=400)
+        else:
+            errors = list(serializer.errors.values())[0]
+        return Response({'detail': errors[0]}, status=400)
 
     # merge conflict
     other_thread = serializer.validated_data['other_thread']
@@ -80,24 +94,53 @@ def threads_merge_endpoint(request):
         },
     )
 
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        if 'threads' in serializer.errors:
+            errors = {'detail': serializer.errors['threads'][0]}
+            return Response(errors, status=403)
+        elif 'non_field_errors' in serializer.errors:
+            errors = {'detail': serializer.errors['non_field_errors'][0]}
+            return Response(errors, status=403)
+        else:
+            return Response(serializer.errors, status=400)
 
     threads = serializer.validated_data['threads']
+    invalid_threads = []
 
-    data = serializer.validated_data
-    threads = data['threads']
+    for thread in threads:
+        try:
+            allow_merge_thread(request.user, thread)
+        except PermissionDenied as e:
+            invalid_threads.append({
+                'id': thread.pk,
+                'title': thread.title,
+                'errors': [text_type(e)]
+            })
+
+    if invalid_threads:
+        return Response(invalid_threads, status=403)
+
+    # handle merge conflict
+    merge_conflict = MergeConflict(serializer.validated_data, threads)
+    merge_conflict.is_valid(raise_exception=True)
+
+    new_thread = merge_threads(request, serializer.validated_data, threads, merge_conflict)
+    return Response(ThreadsListSerializer(new_thread).data)
 
+
+def merge_threads(request, validated_data, threads, merge_conflict):
     new_thread = Thread(
-        category=data['category'],
+        category=validated_data['category'],
         started_on=threads[0].started_on,
         last_post_on=threads[0].last_post_on,
     )
 
-    new_thread.set_title(data['title'])
+    new_thread.set_title(validated_data['title'])
     new_thread.save()
 
-    # handle merge conflict
-    best_answer = data.get('best_answer')
+    resolution = merge_conflict.get_resolution()
+
+    best_answer = resolution.get('best_answer')
     if best_answer:
         new_thread.best_answer_id = best_answer.best_answer_id
         new_thread.best_answer_is_protected = best_answer.best_answer_is_protected
@@ -106,7 +149,7 @@ def threads_merge_endpoint(request):
         new_thread.best_answer_marked_by_name = best_answer.best_answer_marked_by_name
         new_thread.best_answer_marked_by_slug = best_answer.best_answer_marked_by_slug
 
-    poll = data.get('poll')
+    poll = resolution.get('poll')
     if poll:
         poll.move(new_thread)
 
@@ -121,7 +164,7 @@ def threads_merge_endpoint(request):
             new_thread,
             'merged',
             {
-                'merged_thread': thread.title
+                'merged_thread': thread.title,
             },
             commit=False,
         )
@@ -129,13 +172,13 @@ def threads_merge_endpoint(request):
     new_thread.synchronize()
     new_thread.save()
 
-    if data.get('weight') == Thread.WEIGHT_GLOBAL:
+    if validated_data.get('weight') == Thread.WEIGHT_GLOBAL:
         moderation.pin_thread_globally(request, new_thread)
-    elif data.get('weight'):
+    elif validated_data.get('weight'):
         moderation.pin_thread_locally(request, new_thread)
-    if data.get('is_hidden', False):
+    if validated_data.get('is_hidden', False):
         moderation.hide_thread(request, new_thread)
-    if data.get('is_closed', False):
+    if validated_data.get('is_closed', False):
         moderation.close_thread(request, new_thread)
 
     if new_thread.category not in categories:
@@ -150,5 +193,4 @@ def threads_merge_endpoint(request):
     new_thread.subscription = None
 
     add_acl(request.user, new_thread)
-
-    return Response(ThreadsListSerializer(new_thread).data)
+    return new_thread

+ 28 - 30
misago/threads/api/threadendpoints/patch.py

@@ -9,13 +9,13 @@ from django.utils import six
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.api.patch import ApiPatch
 from misago.categories.models import Category
 from misago.categories.permissions import allow_browse_category, allow_see_category
 from misago.categories.serializers import CategorySerializer
 from misago.conf import settings
+from misago.core.apipatch import ApiPatch
 from misago.core.shortcuts import get_int_or_404
-from misago.threads import moderation
+from misago.threads.moderation import threads as moderation
 from misago.threads.participants import (
     add_participant, change_owner, make_participants_aware, remove_participant
 )
@@ -51,9 +51,12 @@ def patch_title(request, thread, value):
     try:
         value_cleaned = six.text_type(value).strip()
     except (TypeError, ValueError):
-        raise ValidationError(_('Not a valid string.'))
+        raise PermissionDenied(_('Not a valid string.'))
 
-    validate_title(value_cleaned)
+    try:
+        validate_title(value_cleaned)
+    except ValidationError as e:
+        raise PermissionDenied(e.args[0])
 
     allow_edit_thread(request.user, thread)
 
@@ -100,7 +103,7 @@ def patch_move(request, thread, value):
     allow_start_thread(request.user, new_category)
 
     if new_category == thread.category:
-        raise ValidationError(_("You can't move thread to the category it's already in."))
+        raise PermissionDenied(_("You can't move thread to the category it's already in."))
 
     moderation.move_thread(request, thread, new_category)
 
@@ -201,7 +204,7 @@ def patch_best_answer(request, thread, value):
     try:
         post_id = int(value)
     except (TypeError, ValueError):
-        raise ValidationError(_("A valid integer is required."))
+        raise PermissionDenied(_("A valid integer is required."))
 
     allow_mark_best_answer(request.user, thread)
 
@@ -238,7 +241,7 @@ def patch_unmark_best_answer(request, thread, value):
     try:
         post_id = int(value)
     except (TypeError, ValueError):
-        raise ValidationError(_("A valid integer is required."))
+        raise PermissionDenied(_("A valid integer is required."))
 
     post = get_object_or_404(thread.post_set, id=post_id)
     post.category = thread.category
@@ -271,26 +274,17 @@ def patch_add_participant(request, thread, value):
     try:
         username = six.text_type(value).strip().lower()
         if not username:
-            raise ValidationError(_("You have to enter new participant's username."))
+            raise PermissionDenied(_("You have to enter new participant's username."))
         participant = UserModel.objects.get(slug=username)
     except UserModel.DoesNotExist:
-        raise ValidationError(_("No user with such name exists."))
+        raise PermissionDenied(_("No user with such name exists."))
 
     if participant in [p.user for p in thread.participants_list]:
-        raise ValidationError(_("This user is already thread participant."))
-
-    max_participants = request.user.acl_cache['max_private_thread_participants']
-    if max_participants:
-        current_participants = len(thread.participants_list)
-        if current_participants >= max_participants:
-            raise ValidationError(_("You can't add any more new users to this thread."))
-    
-    try:
-        allow_add_participant(request.user, participant)
-    except PermissionDenied as e:
-        raise ValidationError(six.text_type(e))
+        raise PermissionDenied(_("This user is already thread participant."))
 
+    allow_add_participant(request.user, participant)
     add_participant(request, thread, participant)
+
     make_participants_aware(request.user, thread)
     participants = ThreadParticipantSerializer(thread.participants_list, many=True)
 
@@ -303,14 +297,14 @@ thread_patch_dispatcher.add('participants', patch_add_participant)
 def patch_remove_participant(request, thread, value):
     try:
         user_id = int(value)
-    except (TypeError, ValueError):
-        raise ValidationError(_("A valid integer is required."))
+    except (ValueError, TypeError):
+        raise PermissionDenied(_("A valid integer is required."))
 
     for participant in thread.participants_list:
         if participant.user_id == user_id:
             break
     else:
-        raise ValidationError(_("Participant doesn't exist."))
+        raise PermissionDenied(_("Participant doesn't exist."))
 
     allow_remove_participant(request.user, thread, participant.user)
     remove_participant(request, thread, participant.user)
@@ -333,17 +327,17 @@ thread_patch_dispatcher.remove('participants', patch_remove_participant)
 def patch_replace_owner(request, thread, value):
     try:
         user_id = int(value)
-    except (TypeError, ValueError):
-        raise ValidationError(_("A valid integer is required."))
+    except (ValueError, TypeError):
+        raise PermissionDenied(_("A valid integer is required."))
 
     for participant in thread.participants_list:
         if participant.user_id == user_id:
             if participant.is_owner:
-                raise ValidationError(_("This user already is thread owner."))
+                raise PermissionDenied(_("This user already is thread owner."))
             else:
                 break
     else:
-        raise ValidationError(_("Participant doesn't exist."))
+        raise PermissionDenied(_("Participant doesn't exist."))
 
     allow_change_owner(request.user, thread)
     change_owner(request, thread, participant.user)
@@ -390,7 +384,8 @@ def thread_patch_endpoint(request, thread):
 
 def bulk_patch_endpoint(request, viewmodel):
     serializer = BulkPatchSerializer(data=request.data)
-    serializer.is_valid(raise_exception=True)
+    if not serializer.is_valid():
+        return Response(serializer.errors, status=400)
 
     threads = clean_threads_for_patch(request, viewmodel, serializer.data['ids'])
 
@@ -446,7 +441,10 @@ def bulk_patch_endpoint(request, viewmodel):
 def clean_threads_for_patch(request, viewmodel, threads_ids):
     threads = []
     for thread_id in sorted(set(threads_ids), reverse=True):
-        threads.append(viewmodel(request, thread_id).unwrap())
+        try:
+            threads.append(viewmodel(request, thread_id).unwrap())
+        except (Http404, PermissionDenied):
+            raise PermissionDenied(_("One or more threads to update could not be found."))
     return threads
 
 

+ 5 - 1
misago/threads/api/threadpoll.py

@@ -15,6 +15,7 @@ from misago.threads.permissions import (
 from misago.threads.serializers import (
     EditPollSerializer, NewPollSerializer, PollSerializer, PollVoteSerializer)
 from misago.threads.viewmodels import ForumThread
+from misago.users.audittrail import create_audit_trail
 
 from .pollvotecreateendpoint import poll_vote_create
 
@@ -60,7 +61,6 @@ class ViewSet(viewsets.ViewSet):
             poster=request.user,
             poster_name=request.user.username,
             poster_slug=request.user.slug,
-            poster_ip=request.user_ip,
         )
 
         serializer = NewPollSerializer(instance, data=request.data)
@@ -75,6 +75,8 @@ class ViewSet(viewsets.ViewSet):
         thread.has_poll = True
         thread.save()
 
+        create_audit_trail(request, instance)
+
         return Response(PollSerializer(instance).data)
 
     @transaction.atomic
@@ -92,6 +94,8 @@ class ViewSet(viewsets.ViewSet):
         add_acl(request.user, instance)
         instance.make_choices_votes_aware(request.user)
 
+        create_audit_trail(request, instance)
+
         return Response(PollSerializer(instance).data)
 
     @transaction.atomic

+ 4 - 4
misago/threads/api/threads.py

@@ -6,10 +6,10 @@ from django.core.exceptions import PermissionDenied
 from django.db import transaction
 from django.utils.translation import ugettext as _
 
-from misago.categories.models import PRIVATE_THREADS_ROOT, THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
 from misago.core.shortcuts import get_int_or_404
 from misago.threads.models import Post, Thread
-from misago.threads import moderation
+from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import allow_use_private_threads
 from misago.threads.viewmodels import (ForumThread, PrivateThread,
     ThreadsRootCategory, PrivateThreadsCategory)
@@ -77,7 +77,7 @@ class ThreadViewSet(ViewSet):
         posting = PostingEndpoint(
             request,
             PostingEndpoint.START,
-            tree_name=THREADS_ROOT,
+            tree_name=THREADS_ROOT_NAME,
             thread=thread,
             post=post,
         )
@@ -132,7 +132,7 @@ class PrivateThreadViewSet(ViewSet):
         posting = PostingEndpoint(
             request,
             PostingEndpoint.START,
-            tree_name=PRIVATE_THREADS_ROOT,
+            tree_name=PRIVATE_THREADS_ROOT_NAME,
             thread=thread,
             post=post,
         )

+ 1 - 1
misago/threads/checksums.py

@@ -9,7 +9,7 @@ def is_post_valid(post):
 
 
 def make_post_checksum(post):
-    post_seeds = [six.text_type(v) for v in (post.id, post.poster_ip)]
+    post_seeds = [six.text_type(v) for v in (post.id, str(post.posted_on.date()))]
     return checksums.make_checksum(post.parsed, post_seeds)
 
 

+ 13 - 0
misago/threads/context_processors.py

@@ -0,0 +1,13 @@
+from django.urls import reverse
+
+
+def preload_threads_urls(request):
+    request.frontend_context.update({
+        'ATTACHMENTS_API': reverse('misago:api:attachment-list'),
+        'THREAD_EDITOR_API': reverse('misago:api:thread-editor'),
+        'THREADS_API': reverse('misago:api:thread-list'),
+        'PRIVATE_THREADS_API': reverse('misago:api:private-thread-list'),
+        'PRIVATE_THREADS_URL': reverse('misago:private-threads'),
+    })
+
+    return {}

+ 0 - 1
misago/threads/events.py

@@ -13,7 +13,6 @@ def record_event(request, thread, event_type, context=None, commit=True):
         thread=thread,
         poster=request.user,
         poster_name=request.user.username,
-        poster_ip=request.user_ip,
         original='-',
         parsed='-',
         posted_on=time_now,

+ 7 - 9
misago/threads/management/commands/clearattachments.py

@@ -28,18 +28,16 @@ class Command(BaseCommand):
             self.sync_attachments(queryset, attachments_to_sync)
 
     def sync_attachments(self, queryset, attachments_to_sync):
-        message = "Clearing %s attachments...\n"
-        self.stdout.write(message % attachments_to_sync)
+        self.stdout.write("Clearing {} attachments...\n".format(attachments_to_sync))
 
-        message = "\n\nCleared %s attachments"
-
-        synchronized_count = 0
-        show_progress(self, synchronized_count, attachments_to_sync)
+        cleared_count = 0
+        show_progress(self, cleared_count, attachments_to_sync)
         start_time = time.time()
+        
         for attachment in chunk_queryset(queryset):
             attachment.delete()
 
-            synchronized_count += 1
-            show_progress(self, synchronized_count, attachments_to_sync, start_time)
+            cleared_count += 1
+            show_progress(self, cleared_count, attachments_to_sync, start_time)
 
-        self.stdout.write(message % synchronized_count)
+        self.stdout.write("\n\nCleared {} attachments".format(cleared_count))

+ 10 - 13
misago/threads/management/commands/rebuildpostssearch.py

@@ -11,21 +11,18 @@ class Command(BaseCommand):
     help = "Rebuilds posts search"
 
     def handle(self, *args, **options):
-        posts_to_sync = Post.objects.filter(is_event=False).count()
+        posts_to_reindex = Post.objects.filter(is_event=False).count()
 
-        if not posts_to_sync:
+        if not posts_to_reindex:
             self.stdout.write("\n\nNo posts were found")
         else:
-            self.sync_threads(posts_to_sync)
+            self.rebuild_posts_search(posts_to_reindex)
 
-    def sync_threads(self, posts_to_sync):
-        message = "Rebuilding %s posts...\n"
-        self.stdout.write(message % posts_to_sync)
+    def rebuild_posts_search(self, posts_to_reindex):
+        self.stdout.write("Rebuilding search for {} posts...\n".format(posts_to_reindex))
 
-        message = "\n\nRebuild %s posts"
-
-        synchronized_count = 0
-        show_progress(self, synchronized_count, posts_to_sync)
+        rebuild_count = 0
+        show_progress(self, rebuild_count, posts_to_reindex)
         start_time = time.time()
 
         queryset = Post.objects.select_related('thread').filter(is_event=False)
@@ -39,7 +36,7 @@ class Command(BaseCommand):
             post.update_search_vector()
             post.save(update_fields=['search_vector'])
 
-            synchronized_count += 1
-            show_progress(self, synchronized_count, posts_to_sync, start_time)
+            rebuild_count += 1
+            show_progress(self, rebuild_count, posts_to_reindex, start_time)
 
-        self.stdout.write(message % synchronized_count)
+        self.stdout.write("\n\nRebuild search for {} posts".format(rebuild_count))

+ 3 - 5
misago/threads/management/commands/synchronizethreads.py

@@ -19,14 +19,12 @@ class Command(BaseCommand):
             self.sync_threads(threads_to_sync)
 
     def sync_threads(self, threads_to_sync):
-        message = "Synchronizing %s threads...\n"
-        self.stdout.write(message % threads_to_sync)
-
-        message = "\n\nSynchronized %s threads"
+        self.stdout.write("Synchronizing {} threads...\n".format(threads_to_sync))
 
         synchronized_count = 0
         show_progress(self, synchronized_count, threads_to_sync)
         start_time = time.time()
+
         for thread in chunk_queryset(Thread.objects.all()):
             thread.synchronize()
             thread.save()
@@ -34,4 +32,4 @@ class Command(BaseCommand):
             synchronized_count += 1
             show_progress(self, synchronized_count, threads_to_sync, start_time)
 
-        self.stdout.write(message % synchronized_count)
+        self.stdout.write("\n\nSynchronized {} threads".format(synchronized_count))

+ 37 - 0
misago/threads/management/commands/updatepostschecksums.py

@@ -0,0 +1,37 @@
+import time
+
+from django.core.management.base import BaseCommand
+
+from misago.core.management.progressbar import show_progress
+from misago.core.pgutils import chunk_queryset
+from misago.threads.checksums import update_post_checksum
+from misago.threads.models import Post
+
+
+class Command(BaseCommand):
+    help = "Updates posts checksums"
+
+    def handle(self, *args, **options):
+        posts_to_update = Post.objects.filter(is_event=False).count()
+
+        if not posts_to_update:
+            self.stdout.write("\n\nNo posts were found")
+        else:
+            self.update_posts_checksums(posts_to_update)
+
+    def update_posts_checksums(self, posts_to_update):
+        self.stdout.write("Updating {} posts checksums...\n".format(posts_to_update))
+
+        updated_count = 0
+        show_progress(self, updated_count, posts_to_update)
+        start_time = time.time()
+
+        queryset = Post.objects.filter(is_event=False)
+        for post in chunk_queryset(queryset):
+            update_post_checksum(post)
+            post.save(update_fields=['checksum'])
+
+            updated_count += 1
+            show_progress(self, updated_count, posts_to_update, start_time)
+
+        self.stdout.write("\n\nUpdated {} posts checksums".format(updated_count))

+ 39 - 0
misago/threads/migrations/0010_auto_20180609_1523.py

@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-09 15:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_threads', '0009_auto_20180326_0010'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='attachment',
+            name='uploader_ip',
+        ),
+        migrations.RemoveField(
+            model_name='poll',
+            name='poster_ip',
+        ),
+        migrations.RemoveField(
+            model_name='pollvote',
+            name='voter_ip',
+        ),
+        migrations.RemoveField(
+            model_name='post',
+            name='poster_ip',
+        ),
+        migrations.RemoveField(
+            model_name='postedit',
+            name='editor_ip',
+        ),
+        migrations.RemoveField(
+            model_name='postlike',
+            name='liker_ip',
+        ),
+    ]

+ 0 - 1
misago/threads/models/attachment.py

@@ -46,7 +46,6 @@ class Attachment(models.Model):
     )
     uploader_name = models.CharField(max_length=255)
     uploader_slug = models.CharField(max_length=255, db_index=True)
-    uploader_ip = models.GenericIPAddressField()
 
     filename = models.CharField(max_length=255, db_index=True)
     size = models.PositiveIntegerField(default=0, db_index=True)

+ 0 - 1
misago/threads/models/poll.py

@@ -24,7 +24,6 @@ class Poll(models.Model):
     )
     poster_name = models.CharField(max_length=255)
     poster_slug = models.CharField(max_length=255)
-    poster_ip = models.GenericIPAddressField()
 
     posted_on = models.DateTimeField(default=timezone.now)
     length = models.PositiveIntegerField(default=0)

+ 0 - 1
misago/threads/models/pollvote.py

@@ -24,7 +24,6 @@ class PollVote(models.Model):
     )
     voter_name = models.CharField(max_length=255)
     voter_slug = models.CharField(max_length=255)
-    voter_ip = models.GenericIPAddressField()
     voted_on = models.DateTimeField(default=timezone.now)
     choice_hash = models.CharField(max_length=12, db_index=True)
 

+ 0 - 1
misago/threads/models/post.py

@@ -34,7 +34,6 @@ class Post(models.Model):
         on_delete=models.SET_NULL,
     )
     poster_name = models.CharField(max_length=255)
-    poster_ip = models.GenericIPAddressField()
     original = models.TextField()
     parsed = models.TextField()
     checksum = models.CharField(max_length=64, default='-')

+ 0 - 1
misago/threads/models/postedit.py

@@ -24,7 +24,6 @@ class PostEdit(models.Model):
     )
     editor_name = models.CharField(max_length=255)
     editor_slug = models.CharField(max_length=255)
-    editor_ip = models.GenericIPAddressField()
 
     edited_from = models.TextField()
     edited_to = models.TextField()

+ 0 - 1
misago/threads/models/postlike.py

@@ -25,7 +25,6 @@ class PostLike(models.Model):
     )
     liker_name = models.CharField(max_length=255, db_index=True)
     liker_slug = models.CharField(max_length=255)
-    liker_ip = models.GenericIPAddressField()
 
     liked_on = models.DateTimeField(default=timezone.now)
 

+ 1 - 1
misago/threads/models/thread.py

@@ -86,7 +86,7 @@ class Thread(models.Model):
         blank=True,
         on_delete=models.SET_NULL,
     )
-    best_answer_is_protected = models.BooleanField(default=False)
+    best_answer_is_protected =  models.BooleanField(default=False)
     best_answer_marked_on = models.DateTimeField(null=True, blank=True)
     best_answer_marked_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,

+ 2 - 5
misago/threads/moderation/__init__.py

@@ -1,6 +1,3 @@
 from .exceptions import ModerationError
-from .threads import (
-    change_thread_title, pin_thread_globally, pin_thread_locally, unpin_thread, move_thread,
-    merge_thread, approve_thread, open_thread, close_thread, unhide_thread, hide_thread,
-    delete_thread)
-from .posts import approve_post, protect_post, unprotect_post, unhide_post, hide_post, delete_post
+from .threads import *
+from .posts import *

+ 10 - 0
misago/threads/moderation/posts.py

@@ -5,6 +5,16 @@ from django.utils.translation import ugettext as _
 from .exceptions import ModerationError
 
 
+__all__ = [
+    'approve_post',
+    'protect_post',
+    'unprotect_post',
+    'unhide_post',
+    'hide_post',
+    'delete_post',
+]
+
+
 def approve_post(user, post):
     if post.is_unapproved:
         post.is_unapproved = False

+ 16 - 0
misago/threads/moderation/threads.py

@@ -4,6 +4,22 @@ from django.utils import timezone
 from misago.threads.events import record_event
 
 
+__all__ = [
+    'change_thread_title',
+    'pin_thread_globally',
+    'pin_thread_locally',
+    'unpin_thread',
+    'move_thread',
+    'merge_thread',
+    'approve_thread',
+    'open_thread',
+    'close_thread',
+    'unhide_thread',
+    'hide_thread',
+    'delete_thread',
+]
+
+
 @transaction.atomic
 def change_thread_title(request, thread, new_title):
     if thread.title != new_title:

+ 2 - 3
misago/threads/participants.py

@@ -149,9 +149,8 @@ def build_noticiation_email(request, thread, user):
     }
 
     return build_mail(
-        request, user, subject % subject_formats, 'misago/emails/privatethread/added', {
-            'thread': thread,
-        }
+        user, subject % subject_formats, 'misago/emails/privatethread/added',
+        sender=request.user, context={'thread': thread}
     )
 
 

+ 9 - 2
misago/threads/permissions/privatethreads.py

@@ -6,7 +6,8 @@ from django.utils.translation import ugettext_lazy as _
 from misago.acl import algebra
 from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
-from misago.categories.models import PRIVATE_THREADS_ROOT, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.core.forms import YesNoSwitch
 from misago.threads.models import Thread
 
@@ -152,7 +153,7 @@ def build_acl(acl, roles, key_name):
 
 
 def add_acl_to_thread(user, thread):
-    if thread.thread_type.root_name != PRIVATE_THREADS_ROOT:
+    if thread.thread_type.root_name != PRIVATE_THREADS_ROOT_NAME:
         return
 
     if not hasattr(thread, 'participant'):
@@ -219,6 +220,12 @@ def allow_add_participants(user, target):
         if target.is_closed:
             raise PermissionDenied(_("Only moderators can add participants to closed threads."))
 
+    max_participants = user.acl_cache['max_private_thread_participants']
+    current_participants = len(target.participants_list) - 1
+
+    if current_participants >= max_participants:
+        raise PermissionDenied(_("You can't add any more new users to this thread."))
+
 
 can_add_participants = return_boolean(allow_add_participants)
 

+ 0 - 52
misago/threads/pollmergehandler.py

@@ -1,52 +0,0 @@
-from django.utils.translation import ugettext as _
-
-from misago.threads.models import Poll
-
-
-class PollMergeHandler(object):
-    def __init__(self, threads):
-        self._list = []
-        self._choices = {0: None}
-
-        self._is_valid = False
-        self._resolution = None
-
-        self.threads = threads
-
-        for thread in threads:
-            try:
-                self._list.append(thread.poll)
-                self._choices[thread.poll.pk] = thread.poll
-            except Poll.DoesNotExist:
-                pass
-
-        self._list.sort(key=lambda choice: choice.thread_id)
-
-    @property
-    def polls(self):
-        return self._list
-
-    def is_merge_conflict(self):
-        return len(self._list) > 1
-
-    def get_available_resolutions(self):
-        resolutions = [[0, _("Delete all polls")]]
-        for poll in self._list:
-            resolutions.append([poll.pk, poll.question])
-        return resolutions
-
-    def set_resolution(self, resolution):
-        try:
-            resolution_clean = int(resolution)
-        except (TypeError, ValueError):
-            return
-
-        if resolution_clean in self._choices:
-            self._resolution = self._choices[resolution_clean]
-            self._is_valid = True
-
-    def is_valid(self):
-        return self._is_valid
-
-    def get_resolution(self):
-        return self._resolution or None

+ 10 - 13
misago/threads/serializers/__init__.py

@@ -1,13 +1,10 @@
-from .moderation import (
-    DeletePostsSerializer, DeleteThreadsSerializer, MergePostsSerializer, MergeThreadSerializer,
-    MergeThreadsSerializer, MovePostsSerializer, NewThreadSerializer, SplitPostsSerializer
-)
-from .threadparticipant import ThreadParticipantSerializer
-from .thread import PrivateThreadSerializer, ThreadSerializer, ThreadsListSerializer
-from .post import PostSerializer
-from .feed import FeedSerializer
-from .postedit import PostEditSerializer
-from .postlike import PostLikeSerializer
-from .attachment import AttachmentSerializer, NewAttachmentSerializer
-from .poll import EditPollSerializer, NewPollSerializer, PollChoiceSerializer, PollSerializer
-from .pollvote import NewVoteSerializer, PollVoteSerializer
+from .moderation import *
+from .threadparticipant import *
+from .thread import *
+from .post import *
+from .feed import *
+from .postedit import *
+from .postlike import *
+from .attachment import *
+from .poll import *
+from .pollvote import *

+ 30 - 108
misago/threads/serializers/attachment.py

@@ -1,141 +1,63 @@
 from rest_framework import serializers
 
-from django.template.defaultfilters import filesizeformat
 from django.urls import reverse
-from django.utils.translation import ugettext as _
 
-from misago.threads.models import Attachment, AttachmentType
+from misago.threads.models import Attachment
 
 
-IMAGE_EXTENSIONS = ('jpg', 'jpeg', 'png', 'gif')
+__all__ = ['AttachmentSerializer']
 
 
 class AttachmentSerializer(serializers.ModelSerializer):
     post = serializers.PrimaryKeyRelatedField(read_only=True)
 
+    acl = serializers.SerializerMethodField()
+    is_image = serializers.SerializerMethodField()
     filetype = serializers.SerializerMethodField()
-    uploader_ip = serializers.SerializerMethodField()
-    has_thumbnail = serializers.SerializerMethodField()
+
+    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Attachment
         fields = [
             'id',
-            'secret',
             'filetype',
             'post',
             'uploaded_on',
             'uploader_name',
-            'uploader_ip',
             'filename',
             'size',
+            'acl',
             'is_image',
-            'has_thumbnail',
+            'url',
         ]
 
-    def get_filetype(self, obj):
-        return obj.filetype.name
-
-    def get_uploader_ip(self, obj):
-        user = self.context.get('user')
-        if user and user.acl_cache['can_see_users_ips']:
-            return obj.uploader_ip
-        else:
+    def get_acl(self, obj):
+        try:
+            return obj.acl
+        except AttributeError:
             return None
 
-    def get_has_thumbnail(self, obj):
-        return bool(obj.thumbnail)
-
-
-class NewAttachmentSerializer(serializers.Serializer):
-    upload = serializers.FileField()
-
-    def validate_upload(self, upload):
-        user = self.context['request'].user
-        user_roles = set(r.pk for r in user.get_roles())
-        max_attachment_size = user.acl_cache['max_attachment_size']
-        
-        self.filetype = self.validate_filetype(upload, user_roles)
-        self.validate_filesize(upload, self.filetype, max_attachment_size)
-
-        return upload
-
-    def validate_filetype(self, upload, user_roles):
-        filename = upload.name.strip().lower()
-
-        queryset = AttachmentType.objects.filter(status=AttachmentType.ENABLED)
-        for filetype in queryset.prefetch_related('limit_uploads_to'):
-            for extension in filetype.extensions_list:
-                if filename.endswith('.%s' % extension):
-                    break
-            else:
-                continue
-
-            if filetype.mimetypes_list and upload.content_type not in filetype.mimetypes_list:
-                continue
-
-            if filetype.limit_uploads_to.exists():
-                allowed_roles = set(r.pk for r in filetype.limit_uploads_to.all())
-                if not user_roles & allowed_roles:
-                    continue
-
-            return filetype
+    def get_is_image(self, obj):
+        return obj.is_image
 
-        raise serializers.ValidationError(_("You can't upload files of this type."))
-
-    def validate_filesize(self, upload, filetype, hard_limit):
-        if upload.size > hard_limit * 1024:
-            message = _("You can't upload files larger than %(limit)s (your file has %(upload)s).")
-            raise serializers.ValidationError(
-                message % {
-                    'upload': filesizeformat(upload.size).rstrip('.0'),
-                    'limit': filesizeformat(hard_limit * 1024).rstrip('.0'),
-                }
-            )
+    def get_filetype(self, obj):
+        return obj.filetype.name
 
-        if filetype.size_limit and upload.size > filetype.size_limit * 1024:
-            message = _(
-                "You can't upload files of this type larger than %(limit)s (your file has %(upload)s)."
-            )
-            raise serializers.ValidationError(
-                message % {
-                    'upload': filesizeformat(upload.size).rstrip('.0'),
-                    'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0'),
+    def get_url(self, obj):
+        return {
+            'index': obj.get_absolute_url(),
+            'thumb': obj.get_thumbnail_url(),
+            'uploader': self.get_uploader_url(obj),
+        }
+
+    def get_uploader_url(self, obj):
+        if obj.uploader_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.uploader_slug,
+                    'pk': obj.uploader_id,
                 }
             )
-
-    def is_upload_image(self, upload):
-        filename = upload.name.strip().lower()
-
-        for extension in IMAGE_EXTENSIONS:
-            if filename.endswith('.%s' % extension):
-                return True
-        return False
-
-    def save(self):
-        request = self.context['request']
-        upload = self.validated_data['upload']
-
-        self.instance = Attachment(
-            secret=Attachment.generate_new_secret(),
-            filetype=self.filetype,
-            size=upload.size,
-            uploader=request.user,
-            uploader_name=request.user.username,
-            uploader_slug=request.user.slug,
-            uploader_ip=request.user_ip,
-            filename=upload.name,
-        )
-
-        if self.is_upload_image(upload):
-            try:
-                self.instance.set_image(upload)
-            except IOError:
-                raise serializers.ValidationError({
-                    'upload': [_("Uploaded image was corrupted or invalid.")],
-                })
         else:
-            self.instance.set_file(upload)
-
-        self.instance.save()
-        return self.instance
+            return None

+ 7 - 2
misago/threads/serializers/feed.py

@@ -1,22 +1,27 @@
 from rest_framework import serializers
 
-from misago.api.serializers import MutableFields
 from misago.categories.serializers import CategorySerializer
+from misago.core.serializers import MutableFields
 from misago.threads.models import Post
 from misago.users.serializers import UserSerializer
 
 from .post import PostSerializer
 
 
+__all__ = [
+    'FeedSerializer',
+]
+
 FeedUserSerializer = UserSerializer.subset_fields(
     'id',
     'username',
     'avatars',
+    'url',
     'title',
     'rank',
 )
 
-FeedCategorySerializer = CategorySerializer.subset_fields('name', 'css_class')
+FeedCategorySerializer = CategorySerializer.subset_fields('name', 'css_class', 'url')
 
 
 class FeedSerializer(PostSerializer, MutableFields):

+ 43 - 66
misago/threads/serializers/moderation.py

@@ -1,12 +1,12 @@
 from rest_framework import serializers
-from rest_framework.exceptions import ValidationError
 
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
 from django.http import Http404
+from django.utils import six
 from django.utils.translation import ugettext as _, ugettext_lazy, ungettext
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT
+from misago.categories import THREADS_ROOT_NAME
 from misago.conf import settings
 from misago.threads.mergeconflict import MergeConflict
 from misago.threads.models import Thread
@@ -25,6 +25,18 @@ POSTS_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
 THREADS_LIMIT = settings.MISAGO_THREADS_PER_PAGE + settings.MISAGO_THREADS_TAIL
 
 
+__all__ = [
+    'DeletePostsSerializer',
+    'DeleteThreadsSerializer',
+    'MergePostsSerializer',
+    'MergeThreadSerializer',
+    'MergeThreadsSerializer',
+    'MovePostsSerializer',
+    'NewThreadSerializer',
+    'SplitPostsSerializer',
+]
+
+
 class DeletePostsSerializer(serializers.Serializer):
     error_empty_or_required = ugettext_lazy("You have to specify at least one post to delete.")
 
@@ -95,14 +107,14 @@ class MergePostsSerializer(serializers.Serializer):
         data = list(set(data))
 
         if len(data) < 2:
-            raise ValidationError(self.error_empty_or_required)
+            raise serializers.ValidationError(self.error_empty_or_required)
         if len(data) > POSTS_LIMIT:
             message = ungettext(
                 "No more than %(limit)s post can be merged at single time.",
                 "No more than %(limit)s posts can be merged at single time.",
                 POSTS_LIMIT,
             )
-            raise ValidationError(message % {'limit': POSTS_LIMIT})
+            raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})
 
         user = self.context['user']
         thread = self.context['thread']
@@ -118,7 +130,7 @@ class MergePostsSerializer(serializers.Serializer):
             try:
                 allow_merge_post(user, post)
             except PermissionDenied as e:
-                raise ValidationError(e)
+                raise serializers.ValidationError(e)
 
             if not posts:
                 posts.append(post)
@@ -146,7 +158,7 @@ class MergePostsSerializer(serializers.Serializer):
             posts.append(post)
 
         if len(posts) != len(data):
-            raise ValidationError(_("One or more posts to merge could not be found."))
+            raise serializers.ValidationError(_("One or more posts to merge could not be found."))
 
         return posts
 
@@ -180,14 +192,14 @@ class MovePostsSerializer(serializers.Serializer):
 
         new_thread_id = get_thread_id_from_url(request, data)
         if not new_thread_id:
-            raise ValidationError(_("This is not a valid thread link."))
+            raise serializers.ValidationError(_("This is not a valid thread link."))
         if new_thread_id == thread.pk:
-            raise ValidationError(_("Thread to move posts to is same as current one."))
+            raise serializers.ValidationError(_("Thread to move posts to is same as current one."))
 
         try:
             new_thread = viewmodel(request, new_thread_id).unwrap()
         except Http404:
-            raise ValidationError(
+            raise serializers.ValidationError(
                 _(
                     "The thread you have entered link to doesn't "
                     "exist or you don't have permission to see it."
@@ -195,7 +207,7 @@ class MovePostsSerializer(serializers.Serializer):
             )
 
         if not new_thread.acl['can_reply']:
-            raise ValidationError(_("You can't move posts to threads you can't reply."))
+            raise serializers.ValidationError(_("You can't move posts to threads you can't reply."))
 
         return new_thread
 
@@ -207,7 +219,7 @@ class MovePostsSerializer(serializers.Serializer):
                 "No more than %(limit)s posts can be moved at single time.",
                 POSTS_LIMIT,
             )
-            raise ValidationError(message % {'limit': POSTS_LIMIT})
+            raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})
 
         request = self.context['request']
         thread = self.context['thread']
@@ -224,10 +236,10 @@ class MovePostsSerializer(serializers.Serializer):
                 allow_move_post(request.user, post)
                 posts.append(post)
             except PermissionDenied as e:
-                raise ValidationError(e)
+                raise serializers.ValidationError(e)
 
         if len(posts) != len(data):
-            raise ValidationError(_("One or more posts to move could not be found."))
+            raise serializers.ValidationError(_("One or more posts to move could not be found."))
 
         return posts
 
@@ -250,8 +262,7 @@ class NewThreadSerializer(serializers.Serializer):
     def validate_category(self, category_id):
         self.category = validate_category(self.context['user'], category_id)
         if not can_start_thread(self.context['user'], self.category):
-            raise ValidationError(
-                _("You can't create new threads in selected category."))
+            raise ValidationError(_("You can't create new threads in selected category."))
         return self.category
 
     def validate_weight(self, weight):
@@ -278,8 +289,7 @@ class NewThreadSerializer(serializers.Serializer):
             return is_hidden  # don't validate hidden further if category failed
 
         if is_hidden and not self.category.acl.get('can_hide_threads'):
-            raise ValidationError(
-                _("You don't have permission to hide threads in this category."))
+            raise ValidationError(_("You don't have permission to hide threads in this category."))
         return is_hidden
 
     def validate_is_closed(self, is_closed):
@@ -377,26 +387,24 @@ class DeleteThreadsSerializer(serializers.Serializer):
         threads = []
         errors = []
 
-        sorted_ids = sorted(data, reverse=True)
-
-        for thread_id in sorted_ids:
+        for thread_id in data:
             try:
                 thread = viewmodel(request, thread_id).unwrap()
                 allow_delete_thread(request.user, thread)
                 threads.append(thread)
-            except PermissionDenied as permission_error:
+            except PermissionDenied as e:
                 errors.append({
                     'thread': {
                         'id': thread.id,
                         'title': thread.title
                     },
-                    'error': permission_error,
+                    'error': six.text_type(e)
                 })
             except Http404 as e:
                 pass # skip invisible threads
 
         if errors:
-            raise ValidationError({'details': errors})
+            raise serializers.ValidationError({'details': errors})
 
         if len(threads) != len(data):
             raise ValidationError(_("One or more threads to delete could not be found."))
@@ -438,7 +446,7 @@ class MergeThreadSerializer(serializers.Serializer):
             other_thread = viewmodel(request, other_thread_id).unwrap()
             allow_merge_thread(request.user, other_thread, otherthread=True)
         except PermissionDenied as e:
-            raise ValidationError(e)
+            raise serializers.ValidationError(e)
         except Http404:
             raise ValidationError(
                 _(
@@ -503,54 +511,23 @@ class MergeThreadsSerializer(NewThreadSerializer):
                 POSTS_LIMIT,
             )
             raise ValidationError(message % {'limit': THREADS_LIMIT})
-        return data
-    
-    def get_valid_threads(self, threads_ids):
-        user = self.context['user']
 
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+
         threads_queryset = Thread.objects.filter(
-            id__in=threads_ids,
+            id__in=data,
             category__tree_id=threads_tree_id,
         ).select_related('category').order_by('-id')
 
-        invalid_threads = []
-        valid_threads = []
+        user = self.context['user']
+
+        threads = []
         for thread in threads_queryset:
             add_acl(user, thread)
             if can_see_thread(user, thread):
-                valid_threads.append(thread)
-                try:
-                    allow_merge_thread(user, thread)
-                except PermissionDenied as permission_error:
-                    invalid_threads.append({
-                        'id': thread.id,
-                        'status': 403,
-                        'detail': permission_error
-                    })
-
-        not_found_ids = set(threads_ids) - set([t.id for t in valid_threads])
-        for not_found_id in not_found_ids:
-            invalid_threads.append({
-                'id': not_found_id,
-                'status': 404,
-                'detail': _(
-                    "Requested thread doesn't exist or you don't have permission to see it."
-                ),
-            })
-
-        if invalid_threads:
-            invalid_threads.sort(key=lambda item: item['id'])
-            raise ValidationError({'merge': invalid_threads})
-
-        return valid_threads
+                threads.append(thread)
 
-    def validate(self, data):
-        data['threads'] = self.get_valid_threads(data['threads'])
+        if len(threads) != len(data):
+            raise ValidationError(_("One or more threads to merge could not be found."))
 
-        merge_conflict = MergeConflict(data, data['threads'])
-        merge_conflict.is_valid(raise_exception=True)
-        data.update(merge_conflict.get_resolution())
-        self.merge_conflict = merge_conflict.get_conflicting_fields()
-        
-        return data
+        return threads

+ 42 - 8
misago/threads/serializers/poll.py

@@ -8,18 +8,28 @@ from django.utils.translation import ungettext
 from misago.threads.models import Poll
 
 
+__all__ = [
+    'PollSerializer',
+    'EditPollSerializer',
+    'NewPollSerializer',
+    'PollChoiceSerializer',
+]
+
 MAX_POLL_OPTIONS = 16
 
 
 class PollSerializer(serializers.ModelSerializer):
+    acl = serializers.SerializerMethodField()
     choices = serializers.SerializerMethodField()
-    poster = serializers.SerializerMethodField()
+
+    api = serializers.SerializerMethodField()
+    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Poll
         fields = [
             'id',
-            'poster',
+            'poster_name',
             'posted_on',
             'length',
             'question',
@@ -27,19 +37,43 @@ class PollSerializer(serializers.ModelSerializer):
             'allow_revotes',
             'votes',
             'is_public',
+            'acl',
             'choices',
+            'api',
+            'url',
         ]
 
-    def get_choices(self, obj):
-        return obj.choices
+    def get_api(self, obj):
+        return {
+            'index': obj.get_api_url(),
+            'votes': obj.get_votes_api_url(),
+        }
 
-    def get_poster(self, obj):
+    def get_url(self, obj):
         return {
-            'id': obj.poster_id,
-            'username': obj.poster_name,
-            'slug': obj.poster_slug
+            'poster': self.get_poster_url(obj),
         }
 
+    def get_poster_url(self, obj):
+        if obj.poster_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.poster_slug,
+                    'pk': obj.poster_id,
+                }
+            )
+        else:
+            return None
+
+    def get_acl(self, obj):
+        try:
+            return obj.acl
+        except AttributeError:
+            return None
+
+    def get_choices(self, obj):
+        return obj.choices
+
 
 class EditPollSerializer(serializers.ModelSerializer):
     length = serializers.IntegerField(required=True, min_value=0, max_value=180)

+ 19 - 11
misago/threads/serializers/pollvote.py

@@ -5,6 +5,12 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
 
+__all__ = [
+    'NewVoteSerializer',
+    'PollVoteSerializer',
+]
+
+
 class NewVoteSerializer(serializers.Serializer):
     choices = serializers.ListField(
         child=serializers.CharField(),
@@ -41,24 +47,26 @@ class NewVoteSerializer(serializers.Serializer):
 
 
 class PollVoteSerializer(serializers.Serializer):
-    id = serializers.SerializerMethodField()
-    username = serializers.SerializerMethodField()
-    slug = serializers.SerializerMethodField()
     voted_on = serializers.DateTimeField()
+    username = serializers.SerializerMethodField()
+
+    url = serializers.SerializerMethodField()
 
     class Meta:
         fields = [
-            'id',
-            'username',
-            'slug'
             'voted_on',
+            'username',
+            'url',
         ]
 
-    def get_id(self, obj):
-        return obj['voter_id']
-
     def get_username(self, obj):
         return obj['voter_name']
 
-    def get_slug(self, obj):
-        return obj['voter_slug']
+    def get_url(self, obj):
+        if obj['voter_id']:
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj['voter_id'],
+                    'slug': obj['voter_slug'],
+                }
+            )

+ 60 - 9
misago/threads/serializers/post.py

@@ -2,11 +2,13 @@ from rest_framework import serializers
 
 from django.urls import reverse
 
-from misago.api.serializers import MutableFields
+from misago.core.serializers import MutableFields
 from misago.threads.models import Post
 from misago.users.serializers import UserSerializer as BaseUserSerializer
 
 
+__all__ = ['PostSerializer']
+
 UserSerializer = BaseUserSerializer.subset_fields(
     'id',
     'username',
@@ -17,30 +19,33 @@ UserSerializer = BaseUserSerializer.subset_fields(
     'title',
     'status',
     'posts',
+    'url',
 )
 
 
 class PostSerializer(serializers.ModelSerializer, MutableFields):
     poster = UserSerializer(many=False, read_only=True)
-    poster_ip = serializers.SerializerMethodField()
     content = serializers.SerializerMethodField()
     attachments = serializers.SerializerMethodField()
     last_editor = serializers.PrimaryKeyRelatedField(read_only=True)
     hidden_by = serializers.PrimaryKeyRelatedField(read_only=True)
 
+    acl = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
     is_new = serializers.SerializerMethodField()
     is_liked = serializers.SerializerMethodField()
     last_likes = serializers.SerializerMethodField()
     likes = serializers.SerializerMethodField()
 
+    api = serializers.SerializerMethodField()
+    url = serializers.SerializerMethodField()
+
     class Meta:
         model = Post
         fields = [
             'id',
             'poster',
             'poster_name',
-            'poster_ip',
             'content',
             'attachments',
             'posted_on',
@@ -59,19 +64,16 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
             'is_event',
             'event_type',
             'event_context',
+            'acl',
             'is_liked',
             'is_new',
             'is_read',
             'last_likes',
             'likes',
+            'api',
+            'url',
         ]
 
-    def get_poster_ip(self, obj):
-        if self.context['user'].acl_cache['can_see_users_ips']:
-            return obj.poster_ip
-        else:
-            return None
-
     def get_content(self, obj):
         if obj.is_valid and not obj.is_event and (not obj.is_hidden or obj.acl['can_see_hidden']):
             return obj.content
@@ -81,6 +83,12 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
     def get_attachments(self, obj):
         return obj.attachments_cache
 
+    def get_acl(self, obj):
+        try:
+            return obj.acl
+        except AttributeError:
+            return None
+
     def get_is_liked(self, obj):
         try:
             return obj.is_liked
@@ -118,3 +126,46 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 return obj.likes
         except AttributeError:
             return None
+
+    def get_api(self, obj):
+        api_links = {
+            'index': obj.get_api_url(),
+            'likes': obj.get_likes_api_url(),
+            'editor': obj.get_editor_api_url(),
+            'edits': obj.get_edits_api_url(),
+            'read': obj.get_read_api_url(),
+        }
+
+        if obj.is_event:
+            del api_links['likes']
+
+        return api_links
+
+    def get_url(self, obj):
+        return {
+            'index': obj.get_absolute_url(),
+            'last_editor': self.get_last_editor_url(obj),
+            'hidden_by': self.get_hidden_by_url(obj),
+        }
+
+    def get_last_editor_url(self, obj):
+        if obj.last_editor_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj.last_editor_id,
+                    'slug': obj.last_editor_slug,
+                }
+            )
+        else:
+            return None
+
+    def get_hidden_by_url(self, obj):
+        if obj.hidden_by_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj.hidden_by_id,
+                    'slug': obj.hidden_by_slug,
+                }
+            )
+        else:
+            return None

+ 24 - 2
misago/threads/serializers/postedit.py

@@ -5,20 +5,42 @@ from django.urls import reverse
 from misago.threads.models import PostEdit
 
 
+__all__ = [
+    'PostEditSerializer',
+]
+
+
 class PostEditSerializer(serializers.ModelSerializer):
-    editor = serializers.PrimaryKeyRelatedField(read_only=True)
     diff = serializers.SerializerMethodField()
 
+    url = serializers.SerializerMethodField()
+
     class Meta:
         model = PostEdit
         fields = [
             'id',
             'edited_on',
-            'editor',
             'editor_name',
             'editor_slug',
             'diff',
+            'url',
         ]
 
     def get_diff(self, obj):
         return obj.get_diff()
+
+    def get_url(self, obj):
+        return {
+            'editor': self.get_editor_url(obj),
+        }
+
+    def get_editor_url(self, obj):
+        if obj.editor_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.editor_slug,
+                    'pk': obj.editor_id,
+                }
+            )
+        else:
+            return None

+ 21 - 6
misago/threads/serializers/postlike.py

@@ -5,30 +5,45 @@ from django.urls import reverse
 from misago.threads.models import PostLike
 
 
+__all__ = [
+    'PostLikeSerializer',
+]
+
+
 class PostLikeSerializer(serializers.ModelSerializer):
     avatars = serializers.SerializerMethodField()
     liker_id = serializers.SerializerMethodField()
     username = serializers.SerializerMethodField()
-    slug = serializers.SerializerMethodField()
+
+    url = serializers.SerializerMethodField()
 
     class Meta:
         model = PostLike
         fields = [
             'id',
+            'avatars',
             'liked_on',
             'liker_id',
             'username',
-            'slug',
-            'avatars',
+            'url',
         ]
+
     def get_liker_id(self, obj):
         return obj['liker_id']
 
     def get_username(self, obj):
         return obj['liker_name']
 
-    def get_slug(self, obj):
-        return obj['liker_slug']
-
     def get_avatars(self, obj):
         return obj.get('liker__avatars')
+
+    def get_url(self, obj):
+        if obj['liker_id']:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj['liker_slug'],
+                    'pk': obj['liker_id'],
+                }
+            )
+        else:
+            return None

+ 91 - 32
misago/threads/serializers/thread.py

@@ -2,17 +2,30 @@ from rest_framework import serializers
 
 from django.urls import reverse
 
-from misago.api.serializers import MutableFields
-from misago.categories.serializers import BasicCategorySerializer
+from misago.categories.serializers import CategorySerializer
+from misago.core.serializers import MutableFields
 from misago.threads.models import Thread
 
 from .poll import PollSerializer
 from .threadparticipant import ThreadParticipantSerializer
 
 
+__all__ = [
+    'ThreadSerializer',
+    'PrivateThreadSerializer',
+    'ThreadsListSerializer',
+]
+
+BasicCategorySerializer = CategorySerializer.subset_fields(
+    'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
+    'level', 'lft', 'rght', 'is_read', 'url'
+)
+
+
 class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     category = BasicCategorySerializer(many=False, read_only=True)
 
+    acl = serializers.SerializerMethodField()
     has_unapproved_posts = serializers.SerializerMethodField()
     is_new = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
@@ -22,6 +35,9 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     best_answer_marked_by = serializers.PrimaryKeyRelatedField(read_only=True)
     subscription = serializers.SerializerMethodField()
 
+    api = serializers.SerializerMethodField()
+    url = serializers.SerializerMethodField()
+
     class Meta:
         model = Thread
         fields = [
@@ -46,13 +62,22 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
             'best_answer_marked_by',
             'best_answer_marked_by_name',
             'best_answer_marked_by_slug',
+            'acl',
             'is_new',
             'is_read',
             'path',
             'poll',
             'subscription',
+            'api',
+            'url',
         ]
 
+    def get_acl(self, obj):
+        try:
+            return obj.acl
+        except AttributeError:
+            return {}
+
     def get_has_unapproved_posts(self, obj):
         try:
             acl = obj.acl
@@ -81,13 +106,60 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return None
 
+    def get_api(self, obj):
+        return {
+            'index': obj.get_api_url(),
+            'editor': obj.get_editor_api_url(),
+            'merge': obj.get_merge_api_url(),
+            'poll': obj.get_poll_api_url(),
+            'posts': {
+                'index': obj.get_posts_api_url(),
+                'merge': obj.get_post_merge_api_url(),
+                'move': obj.get_post_move_api_url(),
+                'split': obj.get_post_split_api_url(),
+            },
+        }
+
+    def get_url(self, obj):
+        return {
+            'index': obj.get_absolute_url(),
+            'new_post': obj.get_new_post_url(),
+            'last_post': obj.get_last_post_url(),
+            'best_answer': obj.get_best_answer_url(),
+            'unapproved_post': obj.get_unapproved_post_url(),
+            'starter': self.get_starter_url(obj),
+            'last_poster': self.get_last_poster_url(obj),
+        }
+
+    def get_starter_url(self, obj):
+        if obj.starter_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.starter_slug,
+                    'pk': obj.starter_id,
+                }
+            )
+        return None
+
+    def get_last_poster_url(self, obj):
+        if obj.last_poster_id:
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.last_poster_slug,
+                    'pk': obj.last_poster_id,
+                }
+            )
+        return None
+
 
 class PrivateThreadSerializer(ThreadSerializer):
     participants = serializers.SerializerMethodField()
 
     class Meta:
         model = Thread
-        fields = ThreadSerializer.Meta.fields + ['participants']
+        fields = ThreadSerializer.Meta.fields + [
+            'participants',
+        ]
 
 
 class ThreadsListSerializer(ThreadSerializer):
@@ -105,37 +177,24 @@ class ThreadsListSerializer(ThreadSerializer):
         ]
 
     def get_starter(self, obj):
-        if obj.starter:
-            avatars = obj.starter.avatars
-            real_name = obj.starter.get_real_name()
-        else:
-            avatars = None
-            real_name = None
-
-        return {
-            'id': obj.starter_id,
-            'username': obj.starter_name,
-            'slug': obj.starter_slug,
-            'real_name': real_name,
-            'avatars': avatars,
-        }
+        if obj.starter_id:
+            return {
+                'id': obj.starter_id,
+                'username': obj.starter.username,
+                'real_name': obj.starter.get_real_name(),
+                'avatars': obj.starter.avatars,
+            }
+        return None
 
     def get_last_poster(self, obj):
-        if obj.last_poster:
-            avatars = obj.last_poster.avatars
-            real_name = obj.last_poster.get_real_name()
-        else:
-            avatars = None
-            real_name = None
-
-        return {
-            'id': obj.last_poster_id,
-            'username': obj.last_poster_name,
-            'slug': obj.last_poster_slug,
-            'real_name': real_name,
-            'avatars': avatars,
-        }
-
+        if obj.last_poster_id:
+            return {
+                'id': obj.last_poster_id,
+                'username': obj.last_poster.username,
+                'real_name': obj.last_poster.get_real_name(),
+                'avatars': obj.last_poster.avatars,
+            }
+        return None
 
 
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')

+ 9 - 5
misago/threads/serializers/threadparticipant.py

@@ -3,15 +3,19 @@ from rest_framework import serializers
 from misago.threads.models import ThreadParticipant
 
 
+__all__ = ['ThreadParticipantSerializer']
+
+
 class ThreadParticipantSerializer(serializers.ModelSerializer):
     id = serializers.SerializerMethodField()
     username = serializers.SerializerMethodField()
-    slug = serializers.SerializerMethodField()
     avatars = serializers.SerializerMethodField()
 
+    url = serializers.SerializerMethodField()
+
     class Meta:
         model = ThreadParticipant
-        fields = ['id', 'username', 'slug', 'avatars', 'is_owner']
+        fields = ['id', 'username', 'avatars', 'url', 'is_owner']
 
     def get_id(self, obj):
         return obj.user.id
@@ -19,8 +23,8 @@ class ThreadParticipantSerializer(serializers.ModelSerializer):
     def get_username(self, obj):
         return obj.user.username
 
-    def get_slug(self, obj):
-        return obj.user.slug
-
     def get_avatars(self, obj):
         return obj.user.avatars
+
+    def get_url(self, obj):
+        return obj.user.get_absolute_url()

+ 64 - 22
misago/threads/signals.py

@@ -1,16 +1,18 @@
+from collections import OrderedDict
+
 from django.contrib.auth import get_user_model
 from django.db import transaction
 from django.db.models.signals import pre_delete
 from django.dispatch import Signal, receiver
+from django.utils.translation import ugettext as _
 
 from misago.categories.models import Category
 from misago.categories.signals import delete_category_content, move_category_content
 from misago.core.pgutils import chunk_queryset
-from misago.core.utils import ANONYMOUS_IP
-from misago.users.signals import anonymize_user_content, delete_user_content, username_changed
+from misago.users.signals import (
+    anonymize_user_data, archive_user_data, delete_user_content, username_changed)
 
 from .anonymize import ANONYMIZABLE_EVENTS, anonymize_event, anonymize_post_last_likes
-from .checksums import update_post_checksum
 from .models import Attachment, Poll, PollVote, Post, PostEdit, PostLike, Thread
 
 
@@ -123,23 +125,63 @@ def delete_user_threads(sender, **kwargs):
             category.save()
 
 
-@receiver(anonymize_user_content)
-def anonymize_user(sender, **kwargs):
-    for post in chunk_queryset(sender.post_set):
-        post.poster_ip = ANONYMOUS_IP
-        update_post_checksum(post)
-        post.save(update_fields=['checksum', 'poster_ip'])
-
-    PostEdit.objects.filter(editor=sender).update(editor_ip=ANONYMOUS_IP)
-    PostLike.objects.filter(liker=sender).update(liker_ip=ANONYMOUS_IP)
-
-    Attachment.objects.filter(uploader=sender).update(uploader_ip=ANONYMOUS_IP)
-
-    Poll.objects.filter(poster=sender).update(poster_ip=ANONYMOUS_IP)
-    PollVote.objects.filter(voter=sender).update(voter_ip=ANONYMOUS_IP)
-
-
-@receiver(anonymize_user_content)
+@receiver(archive_user_data)
+def archive_user_attachments(sender, archive=None, **kwargs):
+    queryset = sender.attachment_set.order_by('id')
+    for attachment in chunk_queryset(queryset):
+        archive.add_model_file(
+            attachment.file,
+            prefix=attachment.uploaded_on.strftime('%H%M%S-file'),
+            date=attachment.uploaded_on,
+        )
+        archive.add_model_file(
+            attachment.image,
+            prefix=attachment.uploaded_on.strftime('%H%M%S-image'),
+            date=attachment.uploaded_on,
+        )
+        archive.add_model_file(
+            attachment.thumbnail,
+            prefix=attachment.uploaded_on.strftime('%H%M%S-thumbnail'),
+            date=attachment.uploaded_on,
+        )
+
+
+@receiver(archive_user_data)
+def archive_user_posts(sender, archive=None, **kwargs):
+    queryset = sender.post_set.order_by('id')
+    for post in chunk_queryset(queryset):
+        item_name = post.posted_on.strftime('%H%M%S-post')
+        archive.add_text(item_name, post.parsed, date=post.posted_on)
+
+
+@receiver(archive_user_data)
+def archive_user_posts_edits(sender, archive=None, **kwargs):
+    queryset = PostEdit.objects.filter(post__poster=sender).order_by('id')
+    for post_edit in chunk_queryset(queryset):
+        item_name = post_edit.edited_on.strftime('%H%M%S-post-edit')
+        archive.add_text(item_name, post_edit.edited_from, date=post_edit.edited_on)
+    queryset = sender.postedit_set.exclude(id__in=queryset.values('id')).order_by('id')
+    for post_edit in chunk_queryset(queryset):
+        item_name = post_edit.edited_on.strftime('%H%M%S-post-edit')
+        archive.add_text(item_name, post_edit.edited_from, date=post_edit.edited_on)
+
+
+@receiver(archive_user_data)
+def archive_user_polls(sender, archive=None, **kwargs):
+    queryset = sender.poll_set.order_by('id')
+    for poll in chunk_queryset(queryset):
+        item_name = poll.posted_on.strftime('%H%M%S-poll')
+        archive.add_dict(
+            item_name,
+            OrderedDict([
+                (_("Question"), poll.question),
+                (_("Choices"), u', '.join([c['label'] for c in poll.choices])),
+            ]),
+            date=poll.posted_on,
+        )
+
+
+@receiver(anonymize_user_data)
 def anonymize_user_in_events(sender, **kwargs):
     queryset = Post.objects.filter(
         is_event=True,
@@ -151,13 +193,13 @@ def anonymize_user_in_events(sender, **kwargs):
         anonymize_event(sender, event)
 
 
-@receiver([anonymize_user_content])
+@receiver([anonymize_user_data])
 def anonymize_user_in_likes(sender, **kwargs):
     for post in chunk_queryset(sender.liked_post_set):
         anonymize_post_last_likes(sender, post)
 
 
-@receiver([anonymize_user_content, username_changed])
+@receiver([anonymize_user_data, username_changed])
 def update_usernames(sender, **kwargs):
     Thread.objects.filter(starter=sender).update(
         starter_name=sender.username,

+ 17 - 19
misago/threads/tests/test_anonymize_data.py

@@ -3,7 +3,6 @@ from django.test import RequestFactory
 from django.urls import reverse
 
 from misago.categories.models import Category
-from misago.core.utils import ANONYMOUS_IP
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from misago.threads import testutils
@@ -35,12 +34,12 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         request.user_ip = '127.0.0.1'
 
         request.include_frontend_context = False
-        request.frontend_context = {'conf': {}, 'store': {}, 'url': {}}
+        request.frontend_context = {}
 
         return request
 
     def test_anonymize_changed_owner_event(self):
-        """changed owner event is anonymized by user.anonymize_content"""
+        """changed owner event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request()
 
@@ -48,7 +47,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         change_owner(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='changed_owner')
         self.assertEqual(event.event_context, {
@@ -60,7 +59,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         })
 
     def test_anonymize_added_participant_event(self):
-        """added participant event is anonymized by user.anonymize_content"""
+        """added participant event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request()
 
@@ -68,7 +67,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         add_participant(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='added_participant')
         self.assertEqual(event.event_context, {
@@ -80,7 +79,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         })
 
     def test_anonymize_owner_left_event(self):
-        """owner left event is anonymized by user.anonymize_content"""
+        """owner left event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request(user)
 
@@ -91,7 +90,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='owner_left')
         self.assertEqual(event.event_context, {
@@ -103,7 +102,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         })
 
     def test_anonymize_removed_owner_event(self):
-        """removed owner event is anonymized by user.anonymize_content"""
+        """removed owner event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request()
 
@@ -114,7 +113,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='removed_owner')
         self.assertEqual(event.event_context, {
@@ -126,7 +125,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         })
 
     def test_anonymize_participant_left_event(self):
-        """participant left event is anonymized by user.anonymize_content"""
+        """participant left event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request(user)
 
@@ -137,7 +136,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='participant_left')
         self.assertEqual(event.event_context, {
@@ -149,7 +148,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         })
         
     def test_anonymize_removed_participant_event(self):
-        """removed participant event is anonymized by user.anonymize_content"""
+        """removed participant event is anonymized by user.anonymize_data"""
         user = get_mock_user()
         request = self.get_request()
 
@@ -160,7 +159,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         event = Post.objects.get(event_type='removed_participant')
         self.assertEqual(event.event_context, {
@@ -185,7 +184,7 @@ class AnonymizeLikesTests(AuthenticatedUserTestCase):
         return request
 
     def test_anonymize_user_likes(self):
-        """post's last like is anonymized by user.anonymize_content"""
+        """post's last like is anonymized by user.anonymize_data"""
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category)
         post = testutils.reply_thread(thread)
@@ -196,7 +195,7 @@ class AnonymizeLikesTests(AuthenticatedUserTestCase):
         patch_is_liked(self.get_request(self.user), post, 1)
         patch_is_liked(self.get_request(user), post, 1)
 
-        user.anonymize_content()
+        user.anonymize_data()
 
         last_likes = Post.objects.get(pk=post.pk).last_likes
         self.assertEqual(last_likes, [
@@ -224,14 +223,13 @@ class AnonymizePostsTests(AuthenticatedUserTestCase):
         return request
 
     def test_anonymize_user_posts(self):
-        """post is anonymized by user.anonymize_content"""
+        """post is anonymized by user.anonymize_data"""
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category)
 
         user = get_mock_user()
         post = testutils.reply_thread(thread, poster=user)
-        user.anonymize_content()
+        user.anonymize_data()
 
         anonymized_post = Post.objects.get(pk=post.pk)
-        self.assertEqual(anonymized_post.poster_ip, ANONYMOUS_IP)
         self.assertTrue(anonymized_post.is_valid)

+ 0 - 1
misago/threads/tests/test_attachmentadmin_views.py

@@ -26,7 +26,6 @@ class AttachmentAdminViewsTests(AdminTestCase):
             uploader=self.user,
             uploader_name=self.user.username,
             uploader_slug=self.user.slug,
-            uploader_ip='127.0.0.1',
             filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             file=None,
             image=None,

+ 33 - 49
misago/threads/tests/test_attachments_api.py

@@ -46,18 +46,12 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         self.override_acl({'max_attachment_size': 0})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to upload new files.",
-        })
+        self.assertContains(response, "don't have permission to upload new files", status_code=403)
 
     def test_no_file_uploaded(self):
         """no file uploaded scenario is handled"""
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'upload': ["No file was submitted."],
-        })
+        self.assertContains(response, "No file has been uploaded.", status_code=400)
 
     def test_invalid_extension(self):
         """uploaded file's extension is rejected as invalid"""
@@ -73,10 +67,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["You can't upload files of this type."],
-            })
+        self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_invalid_mime(self):
         """uploaded file's mimetype is rejected as invalid"""
@@ -92,10 +83,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["You can't upload files of this type."],
-            })
+        self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_no_perm_to_type(self):
         """user needs permission to upload files of this type"""
@@ -114,10 +102,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["You can't upload files of this type."],
-            })
+        self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_type_is_locked(self):
         """new uploads for this filetype are locked"""
@@ -134,10 +119,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["You can't upload files of this type."],
-            })
+        self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_type_is_disabled(self):
         """new uploads for this filetype are disabled"""
@@ -154,10 +136,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["You can't upload files of this type."],
-            })
+        self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_upload_too_big_for_type(self):
         """too big uploads are rejected"""
@@ -174,13 +153,10 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': [
-                    "You can't upload files of this type larger "
-                    "than 100.0\xa0KB (your file has 253.9\xa0KB)."
-                ],
-            })
+
+        self.assertContains(
+            response, "can't upload files of this type larger than", status_code=400
+        )
 
     def test_upload_too_big_for_user(self):
         """too big uploads are rejected"""
@@ -198,12 +174,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': [
-                    "You can't upload files larger than 100.0\xa0KB (your file has 253.9\xa0KB)."
-                ],
-            })
+        self.assertContains(response, "can't upload files larger than", status_code=400)
 
     def test_corrupted_image_upload(self):
         """corrupted image upload is handled"""
@@ -218,10 +189,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                     'upload': upload,
                 }
             )
-            self.assertEqual(response.status_code, 400)
-            self.assertEqual(response.json(), {
-                'upload': ["Uploaded image was corrupted or invalid."],
-            })
+        self.assertContains(response, "Uploaded image was corrupted or invalid.", status_code=400)
 
     def test_document_upload(self):
         """successful upload creates orphan attachment"""
@@ -254,7 +222,11 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertFalse(response_json['has_thumbnail'])
+        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
+        self.assertIsNone(response_json['url']['thumb'])
+        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+
+        self.assertEqual(self.user.audittrail_set.count(), 1)
 
         # files associated with attachment are deleted on its deletion
         file_path = attachment.file.path
@@ -293,7 +265,11 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertFalse(response_json['has_thumbnail'])
+        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
+        self.assertIsNone(response_json['url']['thumb'])
+        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+
+        self.assertEqual(self.user.audittrail_set.count(), 1)
 
     def test_large_image_upload(self):
         """successful large image upload creates orphan attachment with thumbnail"""
@@ -329,7 +305,11 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertTrue(response_json['has_thumbnail'])
+        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
+        self.assertEqual(response_json['url']['thumb'], attachment.get_thumbnail_url())
+        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        
+        self.assertEqual(self.user.audittrail_set.count(), 1)
 
         # thumbnail was scaled down
         thumbnail = Image.open(attachment.thumbnail.path)
@@ -380,4 +360,8 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertTrue(response_json['has_thumbnail'])
+        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
+        self.assertEqual(response_json['url']['thumb'], attachment.get_thumbnail_url())
+        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        
+        self.assertEqual(self.user.audittrail_set.count(), 1)

+ 10 - 30
misago/threads/tests/test_attachments_middleware.py

@@ -16,25 +16,6 @@ class RequestMock(object):
         self.data = data or {}
 
 
-def get_middleware_for_testing(**kwargs):
-    mock_kwargs = {
-        'prefix': 'test',
-        'mode': 0,
-
-        'request': None,
-        'user': None,
-
-        'datetime': None,
-        'parsing_result': None,
-
-        'thread': None,
-        'post': None,
-    }
-    mock_kwargs.update(kwargs)
-
-    return AttachmentsMiddleware(**mock_kwargs)
-
-
 class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
     def setUp(self):
         super(AttachmentsMiddlewareTests, self).setUp()
@@ -60,13 +41,12 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             uploader=self.user if user else None,
             uploader_name=self.user.username,
             uploader_slug=self.user.slug,
-            uploader_ip='127.0.0.1',
             filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
         )
 
     def test_use_this_middleware(self):
         """use_this_middleware returns False if we can't upload attachments"""
-        middleware = get_middleware_for_testing(user=self.user)
+        middleware = AttachmentsMiddleware(user=self.user)
 
         self.override_acl({'max_attachment_size': 0})
 
@@ -81,7 +61,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         INPUTS = [{}, {'attachments': []}]
 
         for test_input in INPUTS:
-            middleware = get_middleware_for_testing(
+            middleware = AttachmentsMiddleware(
                 request=RequestMock(test_input),
                 mode=PostingEndpoint.START,
                 user=self.user,
@@ -96,7 +76,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         INPUTS = ['none', ['a', 'b', 123], range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1)]
 
         for test_input in INPUTS:
-            middleware = get_middleware_for_testing(
+            middleware = AttachmentsMiddleware(
                 request=RequestMock({
                     'attachments': test_input
                 }),
@@ -110,7 +90,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
     def test_get_initial_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock(),
             mode=PostingEndpoint.EDIT,
             user=self.user,
@@ -132,7 +112,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
     def test_get_new_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock(),
             mode=PostingEndpoint.EDIT,
             user=self.user,
@@ -163,7 +143,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         attachment = self.mock_attachment(user=False, post=self.post)
         self.assertIsNone(attachment.uploader)
 
-        serializer = get_middleware_for_testing(
+        serializer = AttachmentsMiddleware(
             request=RequestMock({
                 'attachments': []
             }),
@@ -181,7 +161,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             self.mock_attachment(),
         ]
 
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock({
                 'attachments': [a.pk for a in attachments]
             }),
@@ -209,7 +189,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             self.mock_attachment(post=self.post),
         ]
 
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock({
                 'attachments': [attachments[0].pk]
             }),
@@ -241,7 +221,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             self.mock_attachment(),
         ]
 
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock({
                 'attachments': [attachments[0].pk, attachments[1].pk]
             }),
@@ -269,7 +249,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             self.mock_attachment(),
         ]
 
-        middleware = get_middleware_for_testing(
+        middleware = AttachmentsMiddleware(
             request=RequestMock({
                 'attachments': [attachments[0].pk, attachments[2].pk]
             }),

+ 0 - 1
misago/threads/tests/test_attachmenttypeadmin_views.py

@@ -209,7 +209,6 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
             filetype=test_type,
             uploader_name='Bob',
             uploader_slug='bob',
-            uploader_ip='127.0.0.1',
             filename='test.zip',
             file='sad76asd678as687sa.zip'
         )

+ 0 - 3
misago/threads/tests/test_clearattachments.py

@@ -39,7 +39,6 @@ class ClearAttachmentsTests(TestCase):
                 uploaded_on=cutoff,
                 uploader_name='bob',
                 uploader_slug='bob',
-                uploader_ip='127.0.0.1',
                 filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             )
 
@@ -56,7 +55,6 @@ class ClearAttachmentsTests(TestCase):
                 post=post,
                 uploader_name='bob',
                 uploader_slug='bob',
-                uploader_ip='127.0.0.1',
                 filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             )
 
@@ -68,7 +66,6 @@ class ClearAttachmentsTests(TestCase):
                 size=1000,
                 uploader_name='bob',
                 uploader_slug='bob',
-                uploader_ip='127.0.0.1',
                 filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             )
 

+ 1 - 1
misago/threads/tests/test_delete_user_likes.py

@@ -30,7 +30,7 @@ class DeleteUserLikesTests(AuthenticatedUserTestCase):
         return request
 
     def test_anonymize_user_likes(self):
-        """post's last like is anonymized by user.anonymize_content"""
+        """post's last like is anonymized by user.anonymize_data"""
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category)
         post = testutils.reply_thread(thread)

+ 0 - 1
misago/threads/tests/test_events.py

@@ -57,7 +57,6 @@ class EventsAPITests(TestCase):
         self.assertEqual(event_post.event_type, 'announcement')
         self.assertEqual(event_post.event_context, context)
         self.assertEqual(event_post.poster_id, request.user.pk)
-        self.assertEqual(event_post.poster_ip, request.user_ip)
 
     def test_record_event_is_read(self):
         """record_event makes recorded event read to its author"""

+ 2 - 2
misago/threads/tests/test_floodprotection.py

@@ -9,9 +9,9 @@ from misago.threads import testutils
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
-class FloodProtectionTests(AuthenticatedUserTestCase):
+class PostMentionsTests(AuthenticatedUserTestCase):
     def setUp(self):
-        super(FloodProtectionTests, self).setUp()
+        super(PostMentionsTests, self).setUp()
 
         self.category = Category.objects.get(slug='first-category')
         self.thread = testutils.post_thread(category=self.category)

+ 4 - 23
misago/threads/tests/test_floodprotection_middleware.py

@@ -8,32 +8,13 @@ from misago.threads.api.postingendpoint.floodprotection import FloodProtectionMi
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
-def get_middleware_for_testing(**kwargs):
-    mock_kwargs = {
-        'prefix': 'test',
-        'mode': 0,
-
-        'request': None,
-        'user': None,
-
-        'datetime': None,
-        'parsing_result': None,
-
-        'thread': None,
-        'post': None,
-    }
-    mock_kwargs.update(kwargs)
-
-    return FloodProtectionMiddleware(**mock_kwargs)
-
-
 class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase):
     def test_flood_protection_middleware_on_no_posts(self):
         """middleware sets last_posted_on on user"""
         self.user.update_fields = []
         self.assertIsNone(self.user.last_posted_on)
 
-        middleware = get_middleware_for_testing(user=self.user)
+        middleware = FloodProtectionMiddleware(user=self.user)
         middleware.interrupt_posting(None)
 
         self.assertIsNotNone(self.user.last_posted_on)
@@ -45,7 +26,7 @@ class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase):
         original_last_posted_on = timezone.now() - timedelta(days=1)
         self.user.last_posted_on = original_last_posted_on
 
-        middleware = get_middleware_for_testing(user=self.user)
+        middleware = FloodProtectionMiddleware(user=self.user)
         middleware.interrupt_posting(None)
 
         self.assertTrue(self.user.last_posted_on > original_last_posted_on)
@@ -55,12 +36,12 @@ class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase):
         self.user.last_posted_on = timezone.now()
 
         with self.assertRaises(PostingInterrupt):
-            middleware = get_middleware_for_testing(user=self.user)
+            middleware = FloodProtectionMiddleware(user=self.user)
             middleware.interrupt_posting(None)
 
     def test_flood_permission(self):
         """middleware is respects permission to flood for team members"""
         override_acl(self.user, {'can_omit_flood_protection': True})
 
-        middleware = get_middleware_for_testing(user=self.user)
+        middleware = FloodProtectionMiddleware(user=self.user)
         self.assertFalse(middleware.use_this_middleware())

+ 0 - 1
misago/threads/tests/test_participants.py

@@ -33,7 +33,6 @@ class ParticipantsTests(TestCase):
             category=self.category,
             thread=self.thread,
             poster_name='Tester',
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",

+ 0 - 8
misago/threads/tests/test_post_model.py

@@ -37,7 +37,6 @@ class PostModelTests(TestCase):
             thread=self.thread,
             poster=self.user,
             poster_name=self.user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -78,7 +77,6 @@ class PostModelTests(TestCase):
                     thread=self.thread,
                     poster=other_user,
                     poster_name=other_user.username,
-                    poster_ip='127.0.0.1',
                     original="Hello! I am test message!",
                     parsed="<p>Hello! I am test message!</p>",
                     checksum="nope",
@@ -95,7 +93,6 @@ class PostModelTests(TestCase):
                     thread=other_thread,
                     poster=self.user,
                     poster_name=self.user.username,
-                    poster_ip='127.0.0.1',
                     original="Hello! I am test message!",
                     parsed="<p>Hello! I am test message!</p>",
                     checksum="nope",
@@ -112,7 +109,6 @@ class PostModelTests(TestCase):
                     thread=self.thread,
                     poster=self.user,
                     poster_name=self.user.username,
-                    poster_ip='127.0.0.1',
                     original="Hello! I am test message!",
                     parsed="<p>Hello! I am test message!</p>",
                     checksum="nope",
@@ -129,7 +125,6 @@ class PostModelTests(TestCase):
             thread=self.thread,
             poster=self.user,
             poster_name=self.user.username,
-            poster_ip='127.0.0.1',
             original="I am other message!",
             parsed="<p>I am other message!</p>",
             checksum="nope",
@@ -150,7 +145,6 @@ class PostModelTests(TestCase):
             thread=self.thread,
             poster=self.user,
             poster_name=self.user.username,
-            poster_ip='127.0.0.1',
             original="I am other message!",
             parsed="<p>I am other message!</p>",
             checksum="nope",
@@ -171,7 +165,6 @@ class PostModelTests(TestCase):
             thread=self.thread,
             poster=self.user,
             poster_name=self.user.username,
-            poster_ip='127.0.0.1',
             original="I am other message!",
             parsed="<p>I am other message!</p>",
             checksum="nope",
@@ -184,7 +177,6 @@ class PostModelTests(TestCase):
             thread=self.thread,
             poster=self.user,
             poster_name=self.user.username,
-            poster_ip='127.0.0.1',
             original="I am other message!",
             parsed="<p>I am other message!</p>",
             checksum="nope",

+ 54 - 76
misago/threads/tests/test_privatethread_patch_api.py

@@ -42,10 +42,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have to be thread owner to add new participants to it.",
-        })
+
+        self.assertContains(
+            response, "be thread owner to add new participants to it", status_code=400
+        )
 
     def test_add_empty_username(self):
         """path validates username"""
@@ -60,10 +60,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["You have to enter new participant's username."],
-        })
+
+        self.assertContains(
+            response, "You have to enter new participant's username.", status_code=400
+        )
 
     def test_add_nonexistant_user(self):
         """can't user two times"""
@@ -78,10 +78,8 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["No user with such name exists."],
-        })
+
+        self.assertContains(response, "No user with such name exists.", status_code=400)
 
     def test_add_already_participant(self):
         """can't add user that is already participant"""
@@ -96,10 +94,8 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["This user is already thread participant."],
-        })
+
+        self.assertContains(response, "This user is already thread participant", status_code=400)
 
     def test_add_blocking_user(self):
         """can't add user that is already participant"""
@@ -115,10 +111,8 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["BobBoberson is blocking you."],
-        })
+
+        self.assertContains(response, "BobBoberson is blocking you.", status_code=400)
 
     def test_add_no_perm_user(self):
         """can't add user that has no permission to use private threads"""
@@ -135,10 +129,8 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["BobBoberson can't participate in private threads."],
-        })
+
+        self.assertContains(response, "BobBoberson can't participate", status_code=400)
 
     def test_add_too_many_users(self):
         """can't add user that is already participant"""
@@ -159,10 +151,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["You can't add any more new users to this thread."],
-        })
+
+        self.assertContains(
+            response, "You can't add any more new users to this thread.", status_code=400
+        )
 
     def test_add_user_closed_thread(self):
         """adding user to closed thread fails for non-moderator"""
@@ -180,10 +172,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Only moderators can add participants to closed threads.",
-        })
+
+        self.assertContains(
+            response, "Only moderators can add participants to closed threads.", status_code=400
+        )
 
     def test_add_user(self):
         """adding user to thread add user to thread as participant, sets event and emails him"""
@@ -284,10 +276,8 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
-        })
+
+        self.assertContains(response, "A valid integer is required.", status_code=400)
 
     def test_remove_invalid(self):
         """api validates user id type"""
@@ -302,10 +292,8 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
-        })
+
+        self.assertContains(response, "A valid integer is required.", status_code=400)
 
     def test_remove_nonexistant(self):
         """removed user has to be participant"""
@@ -320,10 +308,8 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["Participant doesn't exist."],
-        })
+
+        self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
     def test_remove_not_owner(self):
         """api validates if user trying to remove other user is an owner"""
@@ -339,10 +325,10 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have to be thread owner to remove participants from it.",
-        })
+
+        self.assertContains(
+            response, "be thread owner to remove participants from it", status_code=400
+        )
 
     def test_owner_remove_user_closed_thread(self):
         """api disallows owner to remove other user from closed thread"""
@@ -361,10 +347,10 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Only moderators can remove participants from closed threads.",
-        })
+
+        self.assertContains(
+            response, "moderators can remove participants from closed threads", status_code=400
+        )
 
     def test_user_leave_thread(self):
         """api allows user to remove himself from thread"""
@@ -591,10 +577,8 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
-        })
+
+        self.assertContains(response, "A valid integer is required.", status_code=400)
 
     def test_invalid_user_id(self):
         """api handles invalid user id"""
@@ -609,10 +593,8 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
-        })
+
+        self.assertContains(response, "A valid integer is required.", status_code=400)
 
     def test_nonexistant_user_id(self):
         """api handles nonexistant user id"""
@@ -627,10 +609,8 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["Participant doesn't exist."],
-        })
+
+        self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
     def test_no_permission(self):
         """non-moderator/owner can't change owner"""
@@ -646,10 +626,10 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Only thread owner and moderators can change threads owners.",
-        })
+
+        self.assertContains(
+            response, "thread owner and moderators can change threads owners", status_code=400
+        )
 
     def test_no_change(self):
         """api validates that new owner id is same as current owner"""
@@ -665,10 +645,8 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["This user already is thread owner."],
-        })
+
+        self.assertContains(response, "This user already is thread owner.", status_code=400)
 
     def test_change_closed_thread_owner(self):
         """non-moderator can't change owner in closed thread"""
@@ -687,10 +665,10 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Only moderators can change closed threads owners.",
-        })
+
+        self.assertContains(
+            response, "Only moderators can change closed threads owners.", status_code=400
+        )
 
     def test_owner_change_thread_owner(self):
         """owner can pass thread ownership to other participant"""

+ 2 - 0
misago/threads/tests/test_privatethread_reply_api.py

@@ -37,6 +37,8 @@ class PrivateThreadReplyApiTestCase(PrivateThreadsTestCase):
         self.assertEqual(self.user.threads, 0)
         self.assertEqual(self.user.posts, 0)
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
         # valid user was flagged to sync
         self.assertFalse(UserModel.objects.get(pk=self.user.pk).sync_unread_private_threads)
         self.assertTrue(UserModel.objects.get(pk=self.other_user.pk).sync_unread_private_threads)

+ 9 - 14
misago/threads/tests/test_privatethread_start_api.py

@@ -38,24 +38,19 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't use private threads.",
-        })
+        self.assertContains(response, "You can't use private threads.", status_code=403)
 
     def test_cant_start_private_thread(self):
         """permission to start private thread is validated"""
         override_acl(self.user, {'can_start_private_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't start private threads.",
-        })
+        self.assertContains(response, "You can't start private threads.", status_code=403)
 
     def test_empty_data(self):
         """no data sent handling has no showstoppers"""
         response = self.client.post(self.api_link, data={})
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
@@ -325,13 +320,11 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        
+
         thread = self.user.thread_set.all()[:1][0]
-        self.assertEqual(response.json(), {
-            'id': thread.pk,
-            'title': thread.title,
-            'url': thread.get_absolute_url(),
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['url'], thread.get_absolute_url())
 
         response = self.client.get(thread.get_absolute_url())
         self.assertContains(response, self.category.name)
@@ -358,6 +351,8 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.assertEqual(post.poster_id, self.user.id)
         self.assertEqual(post.poster_name, self.user.username)
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
         # thread has two participants
         self.assertEqual(thread.participants.count(), 2)
 

+ 17 - 25
misago/threads/tests/test_privatethreads_api.py

@@ -18,26 +18,22 @@ class PrivateThreadsListApiTests(PrivateThreadsTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have to sign in to use private threads.",
-        })
+        self.assertContains(response, "sign in to use private threads", status_code=403)
 
     def test_no_permission(self):
         """api requires user to have permission to be able to access it"""
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't use private threads.",
-        })
+        self.assertContains(response, "can't use private threads", status_code=403)
 
     def test_empty_list(self):
         """api has no showstoppers on returning empty list"""
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json()['results'], [])
+
+        response_json = response.json()
+        self.assertEqual(response_json['count'], 0)
 
     def test_thread_visibility(self):
         """only participated threads are returned by private threads api"""
@@ -83,20 +79,14 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have to sign in to use private threads.",
-        })
+        self.assertContains(response, "sign in to use private threads", status_code=403)
 
     def test_no_permission(self):
         """user needs to have permission to see private thread"""
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't use private threads.",
-        })
+        self.assertContains(response, "t use private threads", status_code=403)
 
     def test_no_participant(self):
         """user cant see thread he isn't part of"""
@@ -132,8 +122,8 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
                 {
                     'id': self.user.id,
                     'username': self.user.username,
-                    'slug': self.user.slug,
                     'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
                     'is_owner': True,
                 },
             ]
@@ -153,8 +143,8 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
                 {
                     'id': self.user.id,
                     'username': self.user.username,
-                    'slug': self.user.slug,
                     'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
                     'is_owner': False,
                 },
             ]
@@ -190,17 +180,19 @@ class PrivateThreadDeleteApiTests(PrivateThreadsTestCase):
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete threads in this category.",
-        })
+
+        self.assertEqual(
+            response.json()['detail'], "You can't delete threads in this category."
+        )
 
         self.override_acl({'can_hide_threads': 1})
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete threads in this category.",
-        })
+
+        self.assertEqual(
+            response.json()['detail'], "You can't delete threads in this category."
+        )
 
     def test_delete_thread(self):
         """DELETE to API link with permission deletes thread"""

+ 31 - 31
misago/threads/tests/test_search.py

@@ -28,10 +28,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -40,10 +40,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -56,10 +56,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=ip' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -72,10 +72,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=elit' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -92,10 +92,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=ipsum' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -112,10 +112,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=ipsum' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -128,10 +128,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=ipsum' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -148,10 +148,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=mars atmosphere' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -166,10 +166,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=Mars atmosphere' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -188,10 +188,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=MMM' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('threads', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('threads', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -200,7 +200,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=Marines Medics' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'threads':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)

+ 49 - 64
misago/threads/tests/test_threads_bulkpatch_api.py → misago/threads/tests/test_thread_bulkpatch_api.py

@@ -14,13 +14,13 @@ class ThreadsBulkPatchApiTestCase(ThreadsApiTestCase):
     def setUp(self):
         super(ThreadsBulkPatchApiTestCase, self).setUp()
 
-        self.threads = [
+        self.threads = list(reversed([
             testutils.post_thread(category=self.category),
             testutils.post_thread(category=self.category),
             testutils.post_thread(category=self.category),
-        ]
+        ]))
 
-        self.ids = [t.id for t in self.threads]
+        self.ids = list(reversed([t.id for t in self.threads]))
 
         self.api_link = reverse('misago:api:thread-list')
 
@@ -132,20 +132,22 @@ class BulkPatchSerializerTests(ThreadsBulkPatchApiTestCase):
             'ops': [{}],
         })
 
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "One or more threads to update could not be found.",
+        })
 
     def test_ops_invalid(self):
         """api validates descriptions"""
         response = self.patch(self.api_link, {
-            'ids': self.ids,
+            'ids': self.ids[:1],
             'ops': [{}],
         })
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ['"op" parameter must be defined.'],
-        })
+        self.assertEqual(response.json(), [
+            {'id': self.ids[0], 'detail': ['undefined op']},
+        ])
 
     def test_anonymous_user(self):
         """anonymous users can't use bulk actions"""
@@ -175,14 +177,12 @@ class ThreadAddAclApiTests(ThreadsBulkPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
-        for i, thread_id in enumerate(self.ids):
-            data = response_json[i]
-            self.assertEqual(data['id'], str(thread_id))
-            self.assertEqual(data['status'], '200')
-            self.assertTrue(data['patch']['acl'])
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertTrue(response_json[i]['acl'])
 
 
-class ThreadsBulkChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
+class BulkThreadChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
     def test_change_thread_title(self):
         """api changes thread title and resyncs the category"""
         self.override_acl({'can_edit_threads': 2})
@@ -201,15 +201,11 @@ class ThreadsBulkChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(thread_id),
-                'status': '200',
-                'patch': {
-                    'title': 'Changed the title!',
-                }
-            } for thread_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertEqual(response_json[i]['title'], 'Changed the title!')
 
         for thread in Thread.objects.filter(id__in=self.ids):
             self.assertEqual(thread.title, 'Changed the title!')
@@ -234,19 +230,20 @@ class ThreadsBulkChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
                 ]
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(thread_id),
-                'status': '403',
-                'detail': "You can't edit threads in this category.",
-            } for thread_id in self.ids
-        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertEqual(
+                response_json[i]['detail'],
+                ["You can't edit threads in this category."],
+            )
 
 
-class ThreadsBulkMoveApiTests(ThreadsBulkPatchApiTestCase):
+class BulkThreadMoveApiTests(ThreadsBulkPatchApiTestCase):
     def setUp(self):
-        super(ThreadsBulkMoveApiTests, self).setUp()
+        super(BulkThreadMoveApiTests, self).setUp()
 
         Category(
             name='Category B',
@@ -308,15 +305,11 @@ class ThreadsBulkMoveApiTests(ThreadsBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(thread_id),
-                'status': '200',
-                'patch': {
-                    'category': self.category_b.pk,
-                },
-            } for thread_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertEqual(response_json[i]['category'], self.category_b.pk)
 
         for thread in Thread.objects.filter(id__in=self.ids):
             self.assertEqual(thread.category_id, self.category_b.pk)
@@ -328,7 +321,7 @@ class ThreadsBulkMoveApiTests(ThreadsBulkPatchApiTestCase):
         self.assertEqual(new_category.threads, 3)
 
 
-class ThreadsBulksHideApiTests(ThreadsBulkPatchApiTestCase):
+class BulkThreadsHideApiTests(ThreadsBulkPatchApiTestCase):
     def test_hide_thread(self):
         """api makes it possible to hide thread"""
         self.override_acl({'can_hide_threads': 1})
@@ -347,15 +340,11 @@ class ThreadsBulksHideApiTests(ThreadsBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(thread_id),
-                'status': '200',
-                'patch': {
-                    'is_hidden': True,
-                },
-            } for thread_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertTrue(response_json[i]['is_hidden'])
 
         for thread in Thread.objects.filter(id__in=self.ids):
             self.assertTrue(thread.is_hidden)
@@ -364,7 +353,7 @@ class ThreadsBulksHideApiTests(ThreadsBulkPatchApiTestCase):
         self.assertNotIn(category.last_thread_id, self.ids)
 
 
-class ThreadsBulksApproveApiTests(ThreadsBulkPatchApiTestCase):
+class BulkThreadsApproveApiTests(ThreadsBulkPatchApiTestCase):
     def test_approve_thread(self):
         """api approvse threads and syncs category"""
         for thread in self.threads:
@@ -396,16 +385,12 @@ class ThreadsBulksApproveApiTests(ThreadsBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(thread_id),
-                'status': '200',
-                'patch': {
-                    'is_unapproved': False,
-                    'has_unapproved_posts': False,
-                },
-            } for thread_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, thread in enumerate(self.threads):
+            self.assertEqual(response_json[i]['id'], thread.id)
+            self.assertFalse(response_json[i]['is_unapproved'])
+            self.assertFalse(response_json[i]['has_unapproved_posts'])
 
         for thread in Thread.objects.filter(id__in=self.ids):
             self.assertFalse(thread.is_unapproved)

+ 28 - 46
misago/threads/tests/test_thread_editreply_api.py

@@ -75,10 +75,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.override_acl({'can_edit_posts': 0})
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit posts in this category.",
-        })
+        self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
     def test_cant_edit_other_user_reply(self):
         """permission to edit reply by other users is validated"""
@@ -88,10 +85,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit other users posts in this category.",
-        })
+        self.assertContains(
+            response, "You can't edit other users posts in this category.", status_code=403
+        )
 
     def test_edit_too_old(self):
         """permission to edit reply within timelimit is validated"""
@@ -104,10 +100,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit posts that are older than 1 minute.",
-        })
+        self.assertContains(
+            response, "You can't edit posts that are older than 1 minute.", status_code=403
+        )
 
     def test_closed_category(self):
         """permssion to edit reply in closed category is validated"""
@@ -117,19 +112,15 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.category.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't edit posts in it.",
-        })
+        self.assertContains(
+            response, "This category is closed. You can't edit posts in it.", status_code=403
+        )
 
         # allow to post in closed category
         self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': ['You have to enter a message.'],
-        })
 
     def test_closed_thread(self):
         """permssion to edit reply in closed thread is validated"""
@@ -139,19 +130,15 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.thread.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't edit posts in it.",
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't edit posts in it.", status_code=403
+        )
 
         # allow to post in closed thread
         self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': ['You have to enter a message.'],
-        })
 
     def test_protected_post(self):
         """permssion to edit protected post is validated"""
@@ -161,40 +148,35 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This post is protected. You can't edit it.",
-        })
+        self.assertContains(
+            response, "This post is protected. You can't edit it.", status_code=403
+        )
 
         # allow to post in closed thread
         self.override_acl({'can_protect_posts': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': ['You have to enter a message.'],
-        })
 
     def test_empty_data(self):
         """no data sent handling has no showstoppers"""
         self.override_acl()
 
         response = self.put(self.api_link, data={})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': ['You have to enter a message.'],
-        })
+
+        self.assertContains(response, "You have to enter a message.", status_code=400)
 
     def test_invalid_data(self):
         """api errors for invalid request data"""
         self.override_acl()
 
-        response = self.client.put(self.api_link,'false', content_type="application/json")
+        response = self.client.put(
+            self.api_link,
+            'false',
+            content_type="application/json",
+        )
 
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ['Invalid data. Expected a dictionary, but got bool.'],
-        })
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_edit_event(self):
         """events can't be edited"""
@@ -204,10 +186,8 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link, data={})
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Events can't be edited.",
-        })
+
+        self.assertContains(response, "Events can't be edited.", status_code=403)
 
     def test_post_is_validated(self):
         """post is validated"""
@@ -259,6 +239,8 @@ class EditReplyTests(AuthenticatedUserTestCase):
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, "<p>This is test edit!</p>")
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
         post = self.thread.post_set.order_by('id').last()
         self.assertEqual(post.edits, 1)
         self.assertEqual(post.original, "This is test edit!")

+ 49 - 167
misago/threads/tests/test_thread_merge_api.py

@@ -67,34 +67,27 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.override_acl({'can_merge_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't merge threads in this category.",
-        })
-        
+        self.assertContains(
+            response,
+            "You can't merge threads in this category.",
+            status_code=403
+        )
+
     def test_merge_no_url(self):
         """api validates if thread url was given"""
         self.override_acl({'can_merge_threads': 1})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'other_thread': ["Enter link to new thread."],
-        })
+        self.assertContains(response, "Enter link to new thread.", status_code=400)
 
     def test_invalid_url(self):
         """api validates thread url"""
         self.override_acl({'can_merge_threads': 1})
 
-        response = self.client.post(
-            self.api_link, {
-                'other_thread': self.user.get_absolute_url(),
-            }
-        )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'other_thread': ["This is not a valid thread link."],
+        response = self.client.post(self.api_link, {
+            'other_thread': self.user.get_absolute_url(),
         })
+        self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
     def test_current_other_thread(self):
         """api validates if thread url given is to current thread"""
@@ -105,10 +98,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': self.thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'other_thread': ["You can't merge thread with itself."],
-        })
+        self.assertContains(response, "You can't merge thread with itself.", status_code=400)
 
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
@@ -122,15 +112,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(self.api_link, {
             'other_thread': other_other_thread,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(),
-            {
-                'other_thread': [
-                    "The thread you have entered link to doesn't exist or "
-                    "you don't have permission to see it."
-                ],
-            }
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
         )
 
     def test_other_thread_is_invisible(self):
@@ -145,15 +128,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(),
-            {
-                'other_thread': [
-                    "The thread you have entered link to doesn't exist or "
-                    "you don't have permission to see it."
-                ],
-            },
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
         )
 
     def test_other_thread_isnt_mergeable(self):
@@ -168,11 +144,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'other_thread': ["Other thread can't be merged with."],
-            }
+
+        self.assertContains(
+            response, "Other thread can't be merged with.", status_code=400
         )
 
     def test_thread_category_is_closed(self):
@@ -195,11 +169,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This category is closed. You can't merge it's threads.",
-            }
+        self.assertContains(
+            response,
+            "This category is closed. You can't merge it's threads.",
+            status_code=403,
         )
 
     def test_thread_is_closed(self):
@@ -222,12 +195,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(),
-            {
-                'detail': "This thread is closed. You can't merge it with other threads.",
-            },
+        self.assertContains(
+            response,
+            "This thread is closed. You can't merge it with other threads.",
+            status_code=403,
         )
 
     def test_other_thread_category_is_closed(self):
@@ -250,12 +221,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(),
-            {
-                'other_thread': ["Other thread's category is closed. You can't merge with it."],
-            },
+        self.assertContains(
+            response, "Other thread's category is closed. You can't merge with it.", status_code=400
         )
 
     def test_other_thread_is_closed(self):
@@ -278,12 +245,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(),
-            {
-                'other_thread': ["Other thread is closed and can't be merged with."],
-            }
+        self.assertContains(
+            response, "Other thread is closed and can't be merged with", status_code=400
         )
 
     def test_other_thread_isnt_replyable(self):
@@ -302,12 +265,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(),
-            {
-                'other_thread': ["You can't merge this thread into thread you can't reply."],
-            }
+        self.assertContains(
+            response, "You can't merge this thread into thread you can't reply.", status_code=400
         )
 
     def test_merge_threads(self):
@@ -322,12 +281,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)
@@ -351,12 +305,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # posts reads are kept
         postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
@@ -391,12 +340,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
@@ -426,12 +370,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
@@ -470,12 +409,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
@@ -497,12 +431,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has three posts and an event now
         self.assertEqual(other_thread.post_set.count(), 4)
@@ -531,12 +460,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has three posts and an event now
         self.assertEqual(other_thread.post_set.count(), 4)
@@ -605,9 +529,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'best_answer': ["Invalid choice."],
-        })
+        self.assertEqual(response.json(), {'detail': "Invalid choice."})
 
         # best answers were untouched
         self.assertEqual(self.thread.post_set.count(), 2)
@@ -636,12 +558,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'best_answer': 0,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has four posts and an event now
         self.assertEqual(other_thread.post_set.count(), 5)
@@ -673,12 +590,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'best_answer': self.thread.pk,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has four posts and an event now
         self.assertEqual(other_thread.post_set.count(), 5)
@@ -710,12 +622,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'best_answer': other_thread.pk,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has four posts and an event now
         self.assertEqual(other_thread.post_set.count(), 5)
@@ -741,12 +648,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)
@@ -772,12 +674,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'other_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)
@@ -842,7 +739,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'poll': ["Invalid choice."]})
+        self.assertEqual(response.json(), {'detail': "Invalid choice."})
 
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
@@ -863,12 +760,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'poll': 0,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)
@@ -896,12 +788,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'poll': poll.pk,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)
@@ -936,12 +823,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
                 'poll': other_poll.pk,
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': other_thread.id,
-            'title': other_thread.title,
-            'url': other_thread.get_absolute_url(),
-        })
+        self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts and an event now
         self.assertEqual(other_thread.post_set.count(), 3)

+ 0 - 11
misago/threads/tests/test_thread_model.py

@@ -34,7 +34,6 @@ class ThreadModelTests(TestCase):
             category=self.category,
             thread=self.thread,
             poster_name='Tester',
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -57,7 +56,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -84,7 +82,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -110,7 +107,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -168,7 +164,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="-",
             parsed="-",
             checksum="nope",
@@ -197,7 +192,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -224,7 +218,6 @@ class ThreadModelTests(TestCase):
             category=self.category,
             poster_name='test',
             poster_slug='test',
-            poster_ip='127.0.0.1',
             choices=[],
         )
 
@@ -242,7 +235,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -268,7 +260,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -292,7 +283,6 @@ class ThreadModelTests(TestCase):
             thread=self.thread,
             poster=user,
             poster_name=user.username,
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
@@ -388,7 +378,6 @@ class ThreadModelTests(TestCase):
             category=self.category,
             thread=other_thread,
             poster_name='Admin',
-            poster_ip='127.0.0.1',
             original="Hello! I am other message!",
             parsed="<p>Hello! I am other message!</p>",
             checksum="nope",

+ 299 - 206
misago/threads/tests/test_thread_patch_api.py

@@ -82,10 +82,10 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit threads in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't edit threads in this category.")
 
     def test_change_thread_title_closed_category_no_permission(self):
         """api test permission to edit thread title in closed category"""
@@ -106,11 +106,12 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't edit threads in it."
-        })
+        self.assertEqual(response.status_code, 400)
 
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't edit threads in it."
+        )
 
     def test_change_thread_title_closed_thread_no_permission(self):
         """api test permission to edit closed thread title"""
@@ -131,10 +132,12 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't edit it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't edit it."
+        )
 
     def test_change_thread_title_after_edit_time(self):
         """api cleans, validates and rejects too short title"""
@@ -153,10 +156,12 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit threads that are older than 1 minute."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't edit threads that are older than 1 minute."
+        )
 
     def test_change_thread_title_invalid(self):
         """api cleans, validates and rejects too short title"""
@@ -172,9 +177,12 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
             ]
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["Thread title should be at least 5 characters long (it has 2)."]
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0],
+            "Thread title should be at least 5 characters long (it has 2)."
+        )
 
 
 class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
@@ -218,10 +226,12 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't change threads weights in it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't change threads weights in it."
+        )
 
     def test_pin_thread_closed_no_permission(self):
         """api checks if thread is closed"""
@@ -242,10 +252,12 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't change its weight."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't change its weight."
+        )
 
     def test_unpin_thread(self):
         """api makes it possible to unpin thread"""
@@ -287,10 +299,12 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't pin threads globally in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't pin threads globally in this category."
+        )
 
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 0)
@@ -314,10 +328,12 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't change globally pinned threads weights in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't change globally pinned threads weights in this category."
+        )
 
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 2)
@@ -385,10 +401,12 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't change threads weights in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't change threads weights in this category."
+        )
 
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 0)
@@ -412,10 +430,12 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't change threads weights in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't change threads weights in this category."
+        )
 
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 1)
@@ -487,8 +507,8 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['category'], self.category_b.pk)
+        reponse_json = response.json()
+        self.assertEqual(reponse_json['category'], self.category_b.pk)
 
         self.override_other_acl({})
 
@@ -521,8 +541,8 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['category'], self.category_b.pk)
+        reponse_json = response.json()
+        self.assertEqual(reponse_json['category'], self.category_b.pk)
 
         self.override_other_acl({})
 
@@ -620,11 +640,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't move threads in this category."
-        })
-        
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't move threads in this category."
+        )
+
         self.override_other_acl({})
 
         thread_json = self.get_thread_json()
@@ -650,10 +672,12 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't move it's threads."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't move it's threads."
+        )
 
     def test_move_closed_thread_no_permission(self):
         """api move closed thread with no permission fails"""
@@ -675,10 +699,12 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't move it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't move it."
+        )
 
     def test_move_thread_no_category_access(self):
         """api move thread to category with no access fails"""
@@ -694,8 +720,10 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], 'NOT FOUND')
 
         self.override_other_acl({})
 
@@ -716,10 +744,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': 'You don\'t have permission to browse "Category B" contents.'
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0],
+            'You don\'t have permission to browse "Category B" contents.'
+        )
 
         self.override_other_acl({})
 
@@ -740,10 +771,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to start new threads in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0],
+            "You don't have permission to start new threads in this category."
+        )
 
         self.override_other_acl({})
 
@@ -765,9 +799,11 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
             ]
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'value': ["You can't move thread to the category it's already in."]
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't move thread to the category it's already in."
+        )
 
         self.override_other_acl({})
 
@@ -853,10 +889,12 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to close this thread."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to close this thread."
+        )
 
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_closed'])
@@ -880,10 +918,12 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to open this thread."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to open this thread."
+        )
 
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_closed'])
@@ -954,10 +994,10 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't approve threads in it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "This category is closed. You can't approve threads in it.")
 
     def test_approve_thread_closed_no_permission(self):
         """api checks permission for approving posts in closed categories"""
@@ -987,10 +1027,10 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't approve it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "This thread is closed. You can't approve it.")
 
     def test_unapprove_thread(self):
         """api returns permission error on approval removal"""
@@ -1005,10 +1045,10 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Content approval can't be reversed."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
 
 
 class ThreadHideApiTests(ThreadPatchApiTestCase):
@@ -1027,8 +1067,8 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['is_hidden'])
 
         self.override_acl({'can_hide_threads': 1})
 
@@ -1048,10 +1088,12 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide threads in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide threads in this category."
+        )
 
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_hidden'])
@@ -1072,10 +1114,12 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide other users theads in this category."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide other users theads in this category."
+        )
 
     def test_hide_owned_thread_no_time(self):
         """api forbids non-moderator from hiding other users threads"""
@@ -1098,10 +1142,12 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide threads that are older than 1 minute."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide threads that are older than 1 minute."
+        )
 
     def test_hide_closed_category_no_permission(self):
         """api test permission to hide thread in closed category"""
@@ -1122,10 +1168,12 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't hide threads in it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't hide threads in it."
+        )
 
     def test_hide_closed_thread_no_permission(self):
         """api test permission to hide closed thread"""
@@ -1146,10 +1194,12 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't hide it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't hide it."
+        )
 
 
 class ThreadUnhideApiTests(ThreadPatchApiTestCase):
@@ -1174,8 +1224,8 @@ class ThreadUnhideApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_hidden'])
 
         self.override_acl({'can_hide_threads': 1})
 
@@ -1216,10 +1266,12 @@ class ThreadUnhideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't reveal threads in it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't reveal threads in it."
+        )
 
     def test_unhide_closed_thread_no_permission(self):
         """api test permission to unhide closed thread"""
@@ -1240,10 +1292,12 @@ class ThreadUnhideApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't reveal it."
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't reveal it."
+        )
 
 
 class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
@@ -1261,8 +1315,8 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['subscription'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['subscription'])
 
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['subscription'])
@@ -1284,8 +1338,8 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['subscription'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['subscription'])
 
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['subscription'])
@@ -1307,8 +1361,8 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['subscription'])
+        reponse_json = response.json()
+        self.assertIsNone(reponse_json['subscription'])
 
         thread_json = self.get_thread_json()
         self.assertIsNone(thread_json['subscription'])
@@ -1369,6 +1423,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
             'id': self.thread.id,
+            'detail': ['ok'],
+
             'best_answer': best_answer.id,
             'best_answer_is_protected': False,
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
@@ -1426,11 +1482,12 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to mark best answers in the "First category" category.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1451,12 +1508,13 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to mark best answer in this thread because you didn't "
                 "start it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1497,12 +1555,13 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to mark best answer in this thread because its '
                 'category "First category" is closed.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1540,12 +1599,13 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You can't mark best answer in this thread because it's closed and you don't have "
                 "permission to open it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1580,7 +1640,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
+            'id': self.thread.id,
+            'detail': ["A valid integer is required."],
         })
 
         thread_json = self.get_thread_json()
@@ -1599,8 +1660,11 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'id': self.thread.id,
+            'detail': ["NOT FOUND"],
+        })
 
         thread_json = self.get_thread_json()
         self.assertIsNone(thread_json['best_answer'])
@@ -1620,8 +1684,11 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'id': self.thread.id,
+            'detail': ["NOT FOUND"],
+        })
 
         thread_json = self.get_thread_json()
         self.assertIsNone(thread_json['best_answer'])
@@ -1641,8 +1708,11 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'id': self.thread.id,
+            'detail': ["NOT FOUND"],
+        })
 
         thread_json = self.get_thread_json()
         self.assertIsNone(thread_json['best_answer'])
@@ -1664,9 +1734,10 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "Events can't be marked as best answers.",
+            'id': self.thread.id,
+            'detail': ["Events can't be marked as best answers."],
         })
 
         thread_json = self.get_thread_json()
@@ -1685,9 +1756,10 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "First post in a thread can't be marked as best answer.",
+            'id': self.thread.id,
+            'detail': ["First post in a thread can't be marked as best answer."],
         })
 
         thread_json = self.get_thread_json()
@@ -1708,9 +1780,10 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "Hidden posts can't be marked as best answers.",
+            'id': self.thread.id,
+            'detail': ["Hidden posts can't be marked as best answers."],
         })
 
         thread_json = self.get_thread_json()
@@ -1731,9 +1804,10 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "Unapproved posts can't be marked as best answers.",
+            'id': self.thread.id,
+            'detail': ["Unapproved posts can't be marked as best answers."],
         })
 
         thread_json = self.get_thread_json()
@@ -1754,12 +1828,13 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to mark this post as best answer because a moderator "
                 "has protected it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1806,6 +1881,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
             'id': self.thread.id,
+            'detail': ['ok'],
+
             'best_answer': best_answer.id,
             'best_answer_is_protected': False,
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
@@ -1836,9 +1913,10 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "This post is already marked as thread's best answer.",
+            'id': self.thread.id,
+            'detail': ["This post is already marked as thread's best answer."],
         })
 
         thread_json = self.get_thread_json()
@@ -1859,11 +1937,12 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to mark best answers in the "First category" category.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1884,12 +1963,13 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to change this thread\'s marked answer because it\'s '
                 'in the "First category" category.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1910,12 +1990,13 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to change this thread's marked answer because you are "
                 "not a thread starter."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -1961,12 +2042,13 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to change best answer that was marked for more than "
                 "5 minutes."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2012,12 +2094,13 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to change this thread's best answer because a "
                 "moderator has protected it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2059,10 +2142,7 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Hidden posts can't be marked as best answers.",
-        })
+        self.assertEqual(response.status_code, 400)
         
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['best_answer'], self.best_answer.id)
@@ -2092,6 +2172,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
             'id': self.thread.id,
+            'detail': ['ok'],
+
             'best_answer': None,
             'best_answer_is_protected': False,
             'best_answer_marked_on': None,
@@ -2123,7 +2205,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'value': ["A valid integer is required."],
+            'id': self.thread.id,
+            'detail': ["A valid integer is required."],
         })
 
         thread_json = self.get_thread_json()
@@ -2142,8 +2225,11 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'id': self.thread.id,
+            'detail': ["NOT FOUND"],
+        })
 
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['best_answer'], self.best_answer.id)
@@ -2163,11 +2249,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "This post can't be unmarked because it's not currently marked as best answer."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2186,12 +2273,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to unmark threads answers in the "First category" '
                 'category.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2210,12 +2298,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to unmark this best answer because you are not a "
                 "thread starter."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2259,12 +2348,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to unmark best answer that was marked for more than "
                 "5 minutes."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2308,12 +2398,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 'You don\'t have permission to unmark this best answer because its category '
                 '"First category" is closed.'
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2357,12 +2448,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You can't unmark this thread's best answer because it's closed and you don't "
                 "have permission to open it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()
@@ -2406,12 +2498,13 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': (
+            'id': self.thread.id,
+            'detail': [
                 "You don't have permission to unmark this thread's best answer because a "
                 "moderator has protected it."
-            ),
+            ],
         })
 
         thread_json = self.get_thread_json()

+ 83 - 117
misago/threads/tests/test_thread_pollcreate_api.py

@@ -1,6 +1,5 @@
 from django.urls import reverse
 
-from misago.core.utils import serialize_datetime
 from misago.threads.models import Poll, Thread
 from misago.threads.serializers.poll import MAX_POLL_OPTIONS
 
@@ -14,9 +13,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
@@ -28,7 +24,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
@@ -40,17 +35,13 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_no_permission(self):
         """api validates that user has permission to start poll in thread"""
         self.override_acl({'can_start_polls': 0})
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't start polls.",
-        })
+        self.assertContains(response, "can't start polls", status_code=403)
 
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to start poll in closed thread"""
@@ -60,10 +51,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.thread.save()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't start polls in it.",
-        })
+        self.assertContains(response, "thread is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
@@ -78,10 +66,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.category.save()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't start polls in it.",
-        })
+        self.assertContains(response, "category is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
@@ -96,10 +81,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.thread.save()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't start polls in other users threads.",
-        })
+        self.assertContains(response, "can't start polls in other users threads", status_code=403)
 
         self.override_acl({'can_start_polls': 2})
 
@@ -113,7 +95,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             category=self.category,
             poster_name='Test',
             poster_slug='test',
-            poster_ip='127.0.0.1',
             length=30,
             question='Test',
             choices=[
@@ -125,21 +106,15 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         )
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "There's already a poll in this thread.",
-        })
+        self.assertContains(response, "There's already a poll in this thread.", status_code=403)
 
     def test_empty_data(self):
         """api handles empty request data"""
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(len(response_json), 4)
 
     def test_length_validation(self):
         """api validates poll's length"""
@@ -149,12 +124,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["Ensure this value is greater than or equal to 0."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is greater than or equal to 0."]
+        )
 
         response = self.post(
             self.api_link, data={
@@ -162,12 +136,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["Ensure this value is less than or equal to 180."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is less than or equal to 180."]
+        )
 
     def test_question_validation(self):
         """api validates question length"""
@@ -177,12 +150,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["Ensure this field has no more than 255 characters."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['question'], ["Ensure this field has no more than 255 characters."]
+        )
 
     def test_validate_choice_length(self):
         """api validates single choice length"""
@@ -197,12 +169,9 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["One or more poll choices are invalid."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
         response = self.post(
             self.api_link,
@@ -216,23 +185,19 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["One or more poll choices are invalid."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
         response = self.post(self.api_link, data={'choices': [{'label': 'Choice'}]})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["You need to add at least two choices to a poll."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['choices'], ["You need to add at least two choices to a poll."]
+        )
 
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
@@ -248,25 +213,22 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertEqual(response.status_code, 400)
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': [
-                "You can't add more than %s options to a single poll (added %s)." % error_formats
-            ],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['choices'],
+            ["You can't add more than %s options to a single poll (added %s)." % error_formats]
+        )
 
     def test_allowed_choices_validation(self):
         """api validates allowed choices number"""
         response = self.post(self.api_link, data={'allowed_choices': 0})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["Ensure this value is greater than or equal to 1."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['allowed_choices'], ["Ensure this value is greater than or equal to 1."]
+        )
 
         response = self.post(
             self.api_link,
@@ -285,11 +247,12 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "Number of allowed choices can't be greater than number of all choices."
-            ],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['non_field_errors'],
+            ["Number of allowed choices can't be greater than number of all choices."]
+        )
 
     def test_poll_created(self):
         """api creates public poll if provided with valid data"""
@@ -315,36 +278,39 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.maxDiff = None
 
-        poll = Poll.objects.all()[0]
-        
-        expected_choices = []
-        for choice in poll.choices:
-            expected_choices.append(choice.copy())
-            expected_choices[-1]['selected'] = False
-
-        self.assertEqual(response.json(), {
-            'id': poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(poll.posted_on),
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 0,
-            'is_public': True,
-            'choices': expected_choices,
-        })
-        
-        self.assertEqual(len(poll.choices), 3)
-        self.assertEqual(len(set([c['hash'] for c in poll.choices])), 3)
-        self.assertEqual([c['label'] for c in poll.choices], ['Red', 'Green', 'Blue'])
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+        self.assertEqual(response_json['votes'], 0)
+        self.assertTrue(response_json['is_public'])
+
+        self.assertEqual(len(response_json['choices']), 3)
+        self.assertEqual(len(set([c['hash'] for c in response_json['choices']])), 3)
+        self.assertEqual([c['label'] for c in response_json['choices']], ['Red', 'Green', 'Blue'])
 
         thread = Thread.objects.get(pk=self.thread.pk)
         self.assertTrue(thread.has_poll)
-        self.assertEqual(thread.poll, poll)
+
+        poll = thread.poll
+
+        self.assertEqual(poll.category_id, self.category.id)
+        self.assertEqual(poll.thread_id, self.thread.id)
+        self.assertEqual(poll.poster_id, self.user.id)
+        self.assertEqual(poll.poster_name, self.user.username)
+        self.assertEqual(poll.poster_slug, self.user.slug)
+        self.assertEqual(poll.length, 40)
+        self.assertEqual(poll.question, "Select two best colors")
+        self.assertEqual(poll.allowed_choices, 2)
+        self.assertTrue(poll.allow_revotes)
+        self.assertEqual(poll.votes, 0)
+        self.assertTrue(poll.is_public)
+
+        self.assertEqual(len(poll.choices), 3)
+        self.assertEqual(len(set([c['hash'] for c in poll.choices])), 3)
+        
+        self.assertEqual(self.user.audittrail_set.count(), 1)

+ 8 - 31
misago/threads/tests/test_thread_polldelete_api.py

@@ -20,9 +20,6 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
@@ -36,7 +33,6 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
@@ -50,7 +46,6 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
@@ -64,7 +59,6 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
@@ -78,17 +72,13 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_no_permission(self):
         """api validates that user has permission to delete poll in thread"""
         self.override_acl({'can_delete_polls': 0})
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete polls.",
-        })
+        self.assertContains(response, "can't delete polls", status_code=403)
 
     def test_no_permission_timeout(self):
         """api validates that user's window to delete poll in thread has closed"""
@@ -98,10 +88,9 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete polls that are older than 5 minutes.",
-        })
+        self.assertContains(
+            response, "can't delete polls that are older than 5 minutes", status_code=403
+        )
 
     def test_no_permission_poll_closed(self):
         """api validates that user's window to delete poll in thread has closed"""
@@ -112,10 +101,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This poll is over. You can't delete it.",
-        })
+        self.assertContains(response, "This poll is over", status_code=403)
 
     def test_no_permission_other_user_poll(self):
         """api validates that user has permission to delete other user poll in thread"""
@@ -125,10 +111,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete other users polls in this category.",
-        })
+        self.assertContains(response, "can't delete other users polls", status_code=403)
 
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to delete poll in closed thread"""
@@ -138,10 +121,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         self.thread.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't delete polls in it.",
-        })
+        self.assertContains(response, "thread is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
@@ -156,10 +136,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         self.category.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't delete polls in it.",
-        })
+        self.assertContains(response, "category is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 

+ 148 - 192
misago/threads/tests/test_thread_polledit_api.py

@@ -3,8 +3,6 @@ from datetime import timedelta
 from django.urls import reverse
 from django.utils import timezone
 
-from misago.core.utils import serialize_datetime
-from misago.threads.models import Poll
 from misago.threads.serializers.poll import MAX_POLL_OPTIONS
 
 from .test_thread_poll_api import ThreadPollApiTestCase
@@ -22,9 +20,6 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
@@ -38,7 +33,6 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
@@ -52,7 +46,6 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
@@ -66,7 +59,6 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
@@ -80,17 +72,13 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_no_permission(self):
         """api validates that user has permission to edit poll in thread"""
         self.override_acl({'can_edit_polls': 0})
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit polls.",
-        })
+        self.assertContains(response, "can't edit polls", status_code=403)
 
     def test_no_permission_timeout(self):
         """api validates that user's window to edit poll in thread has closed"""
@@ -100,10 +88,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit polls that are older than 5 minutes.",
-        })
+        self.assertContains(
+            response, "can't edit polls that are older than 5 minutes", status_code=403
+        )
 
     def test_no_permission_poll_closed(self):
         """api validates that user's window to edit poll in thread has closed"""
@@ -114,10 +101,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This poll is over. You can't edit it.",
-        })
+        self.assertContains(response, "This poll is over", status_code=403)
 
     def test_no_permission_other_user_poll(self):
         """api validates that user has permission to edit other user poll in thread"""
@@ -127,10 +111,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't edit other users polls in this category.",
-        })
+        self.assertContains(response, "can't edit other users polls", status_code=403)
 
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to edit poll in closed thread"""
@@ -140,10 +121,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.thread.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't edit polls in it.",
-        })
+        self.assertContains(response, "thread is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
@@ -158,10 +136,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.category.save()
 
         response = self.put(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't edit polls in it.",
-        })
+        self.assertContains(response, "category is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
@@ -172,12 +147,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api handles empty request data"""
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(len(response_json), 4)
 
     def test_length_validation(self):
         """api validates poll's length"""
@@ -187,40 +159,29 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["Ensure this value is greater than or equal to 0."],
-            'allowed_choices': ["This field is required."],
-        })
 
-        response = self.put(
-            self.api_link, data={
-                'length': 200,
-            }
+        response_json = response.json()
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is greater than or equal to 0."]
         )
+
+        response = self.put(self.api_link, data={'length': 200})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["Ensure this value is less than or equal to 180."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is less than or equal to 180."]
+        )
 
     def test_question_validation(self):
         """api validates question length"""
-        response = self.put(
-            self.api_link, data={
-                'question': 'abcd' * 255,
-            }
-        )
+        response = self.put(self.api_link, data={'question': 'abcd' * 255})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["Ensure this field has no more than 255 characters."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['question'], ["Ensure this field has no more than 255 characters."]
+        )
 
     def test_validate_choice_length(self):
         """api validates single choice length"""
@@ -235,12 +196,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["One or more poll choices are invalid."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
         response = self.put(
             self.api_link,
@@ -254,23 +212,27 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["One or more poll choices are invalid."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
-        
+
+        response_json = response.json()
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
+
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
-        response = self.put(self.api_link, data={'choices': [{'label': 'Choice'}]})
+        response = self.put(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["You need to add at least two choices to a poll."],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['choices'], ["You need to add at least two choices to a poll."]
+        )
 
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
@@ -286,25 +248,22 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.assertEqual(response.status_code, 400)
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': [
-                "You can't add more than %s options to a single poll (added %s)." % error_formats
-            ],
-            'length': ["This field is required."],
-            'allowed_choices': ["This field is required."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['choices'],
+            ["You can't add more than %s options to a single poll (added %s)." % error_formats]
+        )
 
     def test_allowed_choices_validation(self):
         """api validates allowed choices number"""
         response = self.put(self.api_link, data={'allowed_choices': 0})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'question': ["This field is required."],
-            'choices': ["This field is required."],
-            'length': ["This field is required."],
-            'allowed_choices': ["Ensure this value is greater than or equal to 1."],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['allowed_choices'], ["Ensure this value is greater than or equal to 1."]
+        )
 
         response = self.put(
             self.api_link,
@@ -323,11 +282,12 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "Number of allowed choices can't be greater than number of all choices."
-            ],
-        })
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['non_field_errors'],
+            ["Number of allowed choices can't be greater than number of all choices."]
+        )
 
     def test_poll_all_choices_replaced(self):
         """api edits all poll choices out"""
@@ -354,33 +314,29 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        poll = Poll.objects.all()[0]
-
-        expected_choices = []
-        for choice in poll.choices:
-            self.assertIn(choice['label'], ["Red", "Green", "Blue"])
-            expected_choices.append(choice.copy())
-            expected_choices[-1]['selected'] = False
-
-        self.assertEqual(response.json(), {
-            'id': poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(poll.posted_on),
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 0,
-            'is_public': False,
-            'choices': expected_choices,
-        })
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+
+        # you can't change poll's type after its posted
+        self.assertFalse(response_json['is_public'])
+
+        # choices were updated
+        self.assertEqual(len(response_json['choices']), 3)
+        self.assertEqual(len(set([c['hash'] for c in response_json['choices']])), 3)
+        self.assertEqual([c['label'] for c in response_json['choices']], ['Red', 'Green', 'Blue'])
+        self.assertEqual([c['votes'] for c in response_json['choices']], [0, 0, 0])
+        self.assertEqual([c['selected'] for c in response_json['choices']], [False, False, False])
 
         # votes were removed
+        self.assertEqual(response_json['votes'], 0)
         self.assertEqual(self.poll.pollvote_set.count(), 0)
+        
+        self.assertEqual(self.user.audittrail_set.count(), 1)
 
     def test_poll_current_choices_edited(self):
         """api edits current poll choices"""
@@ -418,23 +374,22 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        poll = Poll.objects.all()[0]
-
-        self.assertEqual(response.json(), {
-            'id': poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(poll.posted_on),
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 4,
-            'is_public': False,
-            'choices': [
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+
+        # you can't change poll's type after its posted
+        self.assertFalse(response_json['is_public'])
+
+        # choices were updated
+        self.assertEqual(len(response_json['choices']), 4)
+        self.assertEqual(
+            response_json['choices'],
+            [
                 {
                     'hash': 'aaaaaaaaaaaa',
                     'label': 'First',
@@ -460,10 +415,13 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     'selected': True,
                 },
             ],
-        })
+        )
 
         # no votes were removed
+        self.assertEqual(response_json['votes'], 4)
         self.assertEqual(self.poll.pollvote_set.count(), 4)
+        
+        self.assertEqual(self.user.audittrail_set.count(), 1)
 
     def test_poll_some_choices_edited(self):
         """api edits some poll choices"""
@@ -496,23 +454,22 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        poll = Poll.objects.all()[0]
-
-        self.assertEqual(response.json(), {
-            'id': poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(poll.posted_on),
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 1,
-            'is_public': False,
-            'choices': [
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+
+        # you can't change poll's type after its posted
+        self.assertFalse(response_json['is_public'])
+
+        # choices were updated
+        self.assertEqual(len(response_json['choices']), 3)
+        self.assertEqual(
+            response_json['choices'],
+            [
                 {
                     'hash': 'aaaaaaaaaaaa',
                     'label': 'First',
@@ -526,17 +483,20 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     'selected': False,
                 },
                 {
-                    'hash': poll.choices[2]['hash'],
+                    'hash': response_json['choices'][2]['hash'],
                     'label': 'New Option',
                     'votes': 0,
                     'selected': False,
                 },
             ],
-        })
+        )
 
         # no votes were removed
+        self.assertEqual(response_json['votes'], 1)
         self.assertEqual(self.poll.pollvote_set.count(), 1)
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
     def test_moderate_user_poll(self):
         """api edits all poll choices out in other users poll, even if its over"""
         self.override_acl({'can_edit_polls': 2, 'poll_edit_time': 5})
@@ -545,7 +505,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         self.poll.save()
-        
+
         response = self.put(
             self.api_link,
             data={
@@ -569,30 +529,26 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        poll = Poll.objects.all()[0]
-
-        expected_choices = []
-        for choice in poll.choices:
-            self.assertIn(choice['label'], ["Red", "Green", "Blue"])
-            expected_choices.append(choice.copy())
-            expected_choices[-1]['selected'] = False
-
-        self.assertEqual(response.json(), {
-            'id': poll.id,
-            'poster': {
-                'id': None,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(poll.posted_on),
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 0,
-            'is_public': False,
-            'choices': expected_choices,
-        })
-        
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+
+        # you can't change poll's type after its posted
+        self.assertFalse(response_json['is_public'])
+
+        # choices were updated
+        self.assertEqual(len(response_json['choices']), 3)
+        self.assertEqual(len(set([c['hash'] for c in response_json['choices']])), 3)
+        self.assertEqual([c['label'] for c in response_json['choices']], ['Red', 'Green', 'Blue'])
+        self.assertEqual([c['votes'] for c in response_json['choices']], [0, 0, 0])
+        self.assertEqual([c['selected'] for c in response_json['choices']], [False, False, False])
+
         # votes were removed
+        self.assertEqual(response_json['votes'], 0)
         self.assertEqual(self.poll.pollvote_set.count(), 0)
+
+        self.assertEqual(self.user.audittrail_set.count(), 1)

+ 74 - 314
misago/threads/tests/test_thread_pollvotes_api.py

@@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.utils import timezone
 
-from misago.core.utils import serialize_datetime
 from misago.threads.models import Poll
 
 from .test_thread_poll_api import ThreadPollApiTestCase
@@ -30,57 +29,12 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
             }
         )
 
-    def get_votes_json(self):
-        choices_votes = {choice['hash']: [] for choice in self.poll.choices}
-        queryset = self.poll.pollvote_set.order_by('-id').select_related()
-        for vote in queryset:
-            choices_votes[vote.choice_hash].append({
-                'id': vote.voter_id,
-                'username': vote.voter_name,
-                'slug': vote.voter_slug,
-                'voted_on': serialize_datetime(vote.voted_on),
-            })
-        return choices_votes
-
     def test_anonymous(self):
         """api allows guests to get poll votes"""
         self.logout_user()
 
-        votes_json = self.get_votes_json()
-
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 1,
-                'voters': votes_json['aaaaaaaaaaaa'],
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 0,
-                'voters': [],
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 2,
-                'voters': votes_json['gggggggggggg'],
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 1,
-                'voters': votes_json['dddddddddddd'],
-            },
-        ])
-
-        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
-        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
-        self.assertEqual(len(votes_json['gggggggggggg']), 2)
-        self.assertEqual(len(votes_json['dddddddddddd']), 1)
 
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
@@ -94,7 +48,6 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
@@ -108,7 +61,6 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
@@ -122,7 +74,6 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
@@ -136,8 +87,7 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
-        
+
     def test_no_permission(self):
         """api chcecks permission to see poll voters"""
         self.override_acl({'can_always_see_poll_voters': False})
@@ -147,9 +97,6 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You dont have permission to this poll's voters.",
-        })
 
     def test_nonpublic_poll(self):
         """api validates that poll is public"""
@@ -160,47 +107,26 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You dont have permission to this poll's voters.",
-        })
 
     def test_get_votes(self):
         """api returns list of voters"""
-        votes_json = self.get_votes_json()
-
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 1,
-                'voters': votes_json['aaaaaaaaaaaa'],
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 0,
-                'voters': [],
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 2,
-                'voters': votes_json['gggggggggggg'],
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 1,
-                'voters': votes_json['dddddddddddd'],
-            },
-        ])
-
-        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
-        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
-        self.assertEqual(len(votes_json['gggggggggggg']), 2)
-        self.assertEqual(len(votes_json['dddddddddddd']), 1)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json), 4)
+
+        self.assertEqual([c['label'] for c in response_json], ['Alpha', 'Beta', 'Gamma', 'Delta'])
+        self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
+        self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
+
+        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
+                         'bob')
+
+        user = UserModel.objects.get(slug='bob')
+
+        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
+                         user.get_absolute_url())
 
     def test_get_votes_private_poll(self):
         """api returns list of voters on private poll for user with permission"""
@@ -209,41 +135,23 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         self.poll.is_public = False
         self.poll.save()
 
-        votes_json = self.get_votes_json()
-
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 1,
-                'voters': votes_json['aaaaaaaaaaaa'],
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 0,
-                'voters': [],
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 2,
-                'voters': votes_json['gggggggggggg'],
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 1,
-                'voters': votes_json['dddddddddddd'],
-            },
-        ])
-
-        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
-        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
-        self.assertEqual(len(votes_json['gggggggggggg']), 2)
-        self.assertEqual(len(votes_json['dddddddddddd']), 1)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json), 4)
+
+        self.assertEqual([c['label'] for c in response_json], ['Alpha', 'Beta', 'Gamma', 'Delta'])
+        self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
+        self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
+
+        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
+                         'bob')
+
+        user = UserModel.objects.get(slug='bob')
+
+        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
+                         user.get_absolute_url())
 
 
 class ThreadPostVotesTests(ThreadPollApiTestCase):
@@ -274,9 +182,6 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_empty_vote_json(self):
         """api validates if vote that user has made was empty"""
@@ -285,58 +190,37 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.client.post(
             self.api_link, '[]', content_type='application/json'
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ["You have to make a choice."],
-        })
+        self.assertContains(response, "You have to make a choice.", status_code=400)
 
     def test_empty_vote_form(self):
         """api validates if vote that user has made was empty"""
         self.delete_user_votes()
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ["You have to make a choice."],
-        })
+        self.assertContains(response, "You have to make a choice.", status_code=400)
 
     def test_malformed_vote(self):
         """api validates if vote that user has made was correctly structured"""
         self.delete_user_votes()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ['Expected a list of items but got type "dict".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
         response = self.post(self.api_link, data={})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ['Expected a list of items but got type "dict".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
         response = self.post(self.api_link, data='hello')
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ['Expected a list of items but got type "str".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
         response = self.post(self.api_link, data=123)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ['Expected a list of items but got type "int".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_invalid_choices(self):
         """api validates if vote that user has made overlaps with allowed votes"""
         self.delete_user_votes()
 
         response = self.post(self.api_link, data=['lorem', 'ipsum'])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ["One or more of poll choices were invalid."],
-        })
+        self.assertContains(response, "One or more of poll choices were invalid.", status_code=400)
 
     def test_too_many_choices(self):
         """api validates if vote that user has made overlaps with allowed votes"""
@@ -345,23 +229,19 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.poll.save()
 
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'choices': ["This poll disallows voting for more than 1 choice."],
-        })
+        self.assertContains(
+            response, "This poll disallows voting for more than 1 choice.", status_code=400
+        )
 
     def test_revote(self):
         """api validates if user is trying to change vote in poll that disallows revoting"""
         response = self.post(self.api_link, data=['lorem', 'ipsum'])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have already voted in this poll.",
-        })
+        self.assertContains(response, "You have already voted in this poll.", status_code=403)
 
         self.delete_user_votes()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_vote_in_closed_thread(self):
         """api validates is user has permission to vote poll in closed thread"""
@@ -373,15 +253,12 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't vote in it.",
-        })
+        self.assertContains(response, "thread is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_vote_in_closed_category(self):
         """api validates is user has permission to vote poll in closed category"""
@@ -393,15 +270,12 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't vote in it.",
-        })
+        self.assertContains(response, "category is closed", status_code=403)
 
         self.override_acl(category={'can_close_threads': 1})
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_vote_in_finished_poll(self):
         """api valdiates if poll has finished before letting user to vote in it"""
@@ -412,16 +286,13 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This poll is over. You can't vote in it.",
-        })
-        
+        self.assertContains(response, "This poll is over. You can't vote in it.", status_code=403)
+
         self.poll.length = 50
         self.poll.save()
 
         response = self.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_fresh_vote(self):
         """api handles first vote in poll"""
@@ -429,82 +300,25 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': self.poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(self.poll.posted_on),
-            'length': 0,
-            'question': "Lorem ipsum dolor met?",
-            'allowed_choices': 2,
-            'allow_revotes': False,
-            'votes': 4,
-            'is_public': False,
-            'choices': [
-                {
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': 'Alpha',
-                    'selected': True,
-                    'votes': 2
-                },
-                {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Beta',
-                    'selected': True,
-                    'votes': 1
-                },
-                {
-                    'hash': 'gggggggggggg',
-                    'label': 'Gamma',
-                    'selected': False,
-                    'votes': 1
-                },
-                {
-                    'hash': 'dddddddddddd',
-                    'label': 'Delta',
-                    'selected': False,
-                    'votes': 0
-                },
-            ],
-        })
 
         # validate state change
         poll = Poll.objects.get(pk=self.poll.pk)
         self.assertEqual(poll.votes, 4)
-        self.assertEqual(poll.choices, [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 2
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 1
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 1
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 0
-            },
-        ])
+        self.assertEqual([c['votes'] for c in poll.choices], [2, 1, 1, 0])
+
+        for choice in poll.choices:
+            self.assertNotIn('selected', choice)
 
         self.assertEqual(poll.pollvote_set.count(), 4)
 
-        # validate poll disallows for revote
-        response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have already voted in this poll.",
-        })
+        # validate response json
+        response_json = response.json()
+        self.assertEqual(response_json['votes'], 4)
+        self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
+        self.assertEqual([c['selected'] for c in response_json['choices']],
+                         [True, True, False, False])
+
+        self.assertFalse(response_json['acl']['can_vote'])
 
     def test_vote_change(self):
         """api handles vote change"""
@@ -513,76 +327,22 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'id': self.poll.id,
-            'poster': {
-                'id': self.user.id,
-                'username': self.user.username,
-                'slug': self.user.slug,
-            },
-            'posted_on': serialize_datetime(self.poll.posted_on),
-            'length': 0,
-            'question': "Lorem ipsum dolor met?",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'votes': 4,
-            'is_public': False,
-            'choices': [
-                {
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': 'Alpha',
-                    'selected': True,
-                    'votes': 2
-                },
-                {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Beta',
-                    'selected': True,
-                    'votes': 1
-                },
-                {
-                    'hash': 'gggggggggggg',
-                    'label': 'Gamma',
-                    'selected': False,
-                    'votes': 1
-                },
-                {
-                    'hash': 'dddddddddddd',
-                    'label': 'Delta',
-                    'selected': False,
-                    'votes': 0
-                },
-            ],
-        })
 
         # validate state change
         poll = Poll.objects.get(pk=self.poll.pk)
         self.assertEqual(poll.votes, 4)
-        self.assertEqual(poll.choices, [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 2
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 1
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 1
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 0
-            },
-        ])
+        self.assertEqual([c['votes'] for c in poll.choices], [2, 1, 1, 0])
+
+        for choice in poll.choices:
+            self.assertNotIn('selected', choice)
 
         self.assertEqual(poll.pollvote_set.count(), 4)
 
-        # validate poll allows for revote
-        response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
-        self.assertEqual(response.status_code, 200)
+        # validate response json
+        response_json = response.json()
+        self.assertEqual(response_json['votes'], 4)
+        self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
+        self.assertEqual([c['selected'] for c in response_json['choices']],
+                         [True, True, False, False])
+
+        self.assertTrue(response_json['acl']['can_vote'])

+ 30 - 79
misago/threads/tests/test_thread_postbulkdelete_api.py

@@ -35,34 +35,22 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.logout_user()
 
         response = self.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_delete_no_data(self):
         """api handles empty data"""
         response = self.client.delete(self.api_link, content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ['Expected a list of items but got type "dict".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
     def test_delete_no_ids(self):
         """api requires ids to delete"""
         response = self.delete(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to specify at least one post to delete."],
-        })
+        self.assertContains(response, "You have to specify at least one post to delete.", status_code=400)
 
     def test_delete_empty_ids(self):
         """api requires ids to delete"""
         response = self.delete(self.api_link, [])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to specify at least one post to delete."],
-        })
+        self.assertContains(response, "You have to specify at least one post to delete.", status_code=400)
 
     def test_validate_ids(self):
         """api validates that ids are list of ints"""
@@ -72,22 +60,13 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, True)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ['Expected a list of items but got type "bool".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
         response = self.delete(self.api_link, 'abbss')
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ['Expected a list of items but got type "str".'],
-        })
+        self.assertContains(response, "Expected a list of items", status_code=400)
 
         response = self.delete(self.api_link, [1, 2, 3, 'a', 'b', 'x'])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more post ids received were invalid."],
-        })
+        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
 
     def test_validate_ids_length(self):
         """api validates that ids are list of ints"""
@@ -97,10 +76,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, list(range(100)))
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["No more than 24 posts can be deleted at single time."],
-        })
+        self.assertContains(response, "No more than 24 posts can be deleted at single time.", status_code=400)
 
     def test_validate_posts_exist(self):
         """api validates that ids are visible posts"""
@@ -110,10 +86,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, [p.id * 10 for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "One or more posts to delete could not be found.",
-        })
+        self.assertContains(response, "One or more posts to delete could not be found.", status_code=403)
 
     def test_validate_posts_visibility(self):
         """api validates that ids are visible posts"""
@@ -126,10 +99,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.posts[1].save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "One or more posts to delete could not be found.",
-        })
+        self.assertContains(response, "One or more posts to delete could not be found.", status_code=403)
 
     def test_validate_posts_same_thread(self):
         """api validates that ids are same thread posts"""
@@ -142,10 +112,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.posts.append(testutils.reply_thread(other_thread, poster=self.user))
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "One or more posts to delete could not be found.",
-        })
+        self.assertContains(response, "One or more posts to delete could not be found.", status_code=403)
 
     def test_no_permission(self):
         """api validates permission to delete"""
@@ -155,10 +122,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete posts in this category.",
-        })
+        self.assertContains(response, "You can't delete posts in this category.", status_code=403)
 
     def test_delete_other_user_post_no_permission(self):
         """api valdiates if user can delete other users posts"""
@@ -169,10 +133,9 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete other users posts in this category.",
-        })
+        self.assertContains(
+            response, "You can't delete other users posts in this category", status_code=403
+        )
 
     def test_delete_protected_post_no_permission(self):
         """api validates if user can delete protected post"""
@@ -186,10 +149,9 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.posts[0].save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This post is protected. You can't delete it.",
-        })
+        self.assertContains(
+            response, "This post is protected. You can't delete it.", status_code=403
+        )
 
     def test_delete_protected_post_after_edit_time(self):
         """api validates if user can delete delete post after edit time"""
@@ -203,11 +165,10 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.posts[0].save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete posts that are older than 1 minute.",
-        })
-        
+        self.assertContains(
+            response, "You can't delete posts that are older than 1 minute.", status_code=403
+        )
+
     def test_delete_post_closed_thread_no_permission(self):
         """api valdiates if user can delete posts in closed threads"""
         self.override_acl({
@@ -219,10 +180,9 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.thread.save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't delete posts in it.",
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't delete posts in it.", status_code=403
+        )
 
     def test_delete_post_closed_category_no_permission(self):
         """api valdiates if user can delete posts in closed categories"""
@@ -235,10 +195,9 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.category.save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't delete posts in it.",
-        })
+        self.assertContains(
+            response, "This category is closed. You can't delete posts in it.", status_code=403
+        )
 
     def test_delete_first_post(self):
         """api disallows first post's deletion"""
@@ -251,10 +210,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         ids.append(self.thread.first_post_id)
 
         response = self.delete(self.api_link, ids)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete thread's first post.",
-        })
+        self.assertContains(response, "You can't delete thread's first post.", status_code=403)
 
     def test_delete_best_answer(self):
         """api disallows best answer deletion"""
@@ -281,10 +237,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.posts[1].save()
 
         response = self.delete(self.api_link, [p.id for p in self.posts])
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete events in this category.",
-        })
+        self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
     def test_delete_owned_posts(self):
         """api deletes owned thread posts"""
@@ -297,8 +250,6 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         ids = [self.posts[0].id, self.posts[-1].id]
 
         response = self.delete(self.api_link, ids)
-        self.assertEqual(response.status_code, 200)
-        
         self.thread = Thread.objects.get(pk=self.thread.pk)
 
         self.assertNotEqual(self.thread.last_post_id, ids[-1])

+ 25 - 34
misago/threads/tests/test_thread_postbulkpatch_api.py

@@ -170,9 +170,9 @@ class BulkPatchSerializerTests(ThreadPostBulkPatchApiTestCase):
         })
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ['"op" parameter must be defined.'],
-        })
+        self.assertEqual(response.json(), [
+            {'id': self.ids[0], 'detail': ['undefined op']},
+        ])
 
     def test_anonymous_user(self):
         """anonymous users can't use bulk actions"""
@@ -217,11 +217,9 @@ class PostsAddAclApiTests(ThreadPostBulkPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
-        for i, post_id in enumerate(self.ids):
-            data = response_json[i]
-            self.assertEqual(data['id'], str(post_id))
-            self.assertEqual(data['status'], '200')
-            self.assertTrue(data['patch']['acl'])
+        for i, post in enumerate(self.posts):
+            self.assertEqual(response_json[i]['id'], post.id)
+            self.assertTrue(response_json[i]['acl'])
 
 
 class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase):
@@ -245,15 +243,11 @@ class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(post_id),
-                'status': '200',
-                'patch': {
-                    'is_protected': True,
-                },
-            } for post_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, post in enumerate(self.posts):
+            self.assertEqual(response_json[i]['id'], post.id)
+            self.assertTrue(response_json[i]['is_protected'])
 
         for post in Post.objects.filter(id__in=self.ids):
             self.assertTrue(post.is_protected)
@@ -274,14 +268,15 @@ class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase):
                 ]
             }
         )
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(post_id),
-                'status': '403',
-                'detail': "You can't protect posts in this category.",
-            } for post_id in self.ids
-        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        for i, post in enumerate(self.posts):
+            self.assertEqual(response_json[i]['id'], post.id)
+            self.assertEqual(
+                response_json[i]['detail'],
+                ["You can't protect posts in this category."],
+            )
 
         for post in Post.objects.filter(id__in=self.ids):
             self.assertFalse(post.is_protected)
@@ -314,15 +309,11 @@ class BulkPostsApproveApiTests(ThreadPostBulkPatchApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            {
-                'id': str(post_id),
-                'status': '200',
-                'patch': {
-                    'is_unapproved': False,
-                },
-            } for post_id in self.ids
-        ])
+
+        response_json = response.json()
+        for i, post in enumerate(self.posts):
+            self.assertEqual(response_json[i]['id'], post.id)
+            self.assertFalse(response_json[i]['is_unapproved'])
 
         for post in Post.objects.filter(id__in=self.ids):
             self.assertFalse(post.is_unapproved)

+ 26 - 48
misago/threads/tests/test_thread_postdelete_api.py

@@ -28,20 +28,14 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.logout_user()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_no_permission(self):
         """api validates permission to delete post"""
         self.override_acl({'can_hide_own_posts': 1, 'can_hide_posts': 1})
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete posts in this category.",
-        })
+        self.assertContains(response, "You can't delete posts in this category.", status_code=403)
 
     def test_delete_other_user_post_no_permission(self):
         """api valdiates if user can delete other users posts"""
@@ -55,10 +49,9 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.post.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete other users posts in this category.",
-        })
+        self.assertContains(
+            response, "You can't delete other users posts in this category", status_code=403
+        )
 
     def test_delete_protected_post_no_permission(self):
         """api validates if user can delete protected post"""
@@ -72,10 +65,9 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.post.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This post is protected. You can't delete it.",
-        })
+        self.assertContains(
+            response, "This post is protected. You can't delete it.", status_code=403
+        )
 
     def test_delete_protected_post_after_edit_time(self):
         """api validates if user can delete delete post after edit time"""
@@ -89,10 +81,9 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.post.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete posts that are older than 1 minute.",
-        })
+        self.assertContains(
+            response, "You can't delete posts that are older than 1 minute.", status_code=403
+        )
 
     def test_delete_post_closed_thread_no_permission(self):
         """api valdiates if user can delete posts in closed threads"""
@@ -105,10 +96,9 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.thread.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't delete posts in it.",
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't delete posts in it.", status_code=403
+        )
 
     def test_delete_post_closed_category_no_permission(self):
         """api valdiates if user can delete posts in closed categories"""
@@ -121,10 +111,9 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.category.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't delete posts in it.",
-        })
+        self.assertContains(
+            response, "This category is closed. You can't delete posts in it.", status_code=403
+        )
 
     def test_delete_first_post(self):
         """api disallows first post deletion"""
@@ -139,10 +128,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         )
 
         response = self.client.delete(api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete thread's first post.",
-        })
+        self.assertContains(response, "You can't delete thread's first post.", status_code=403)
 
     def test_delete_best_answer(self):
         """api disallows best answer deletion"""
@@ -207,10 +193,7 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         self.logout_user()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_no_permission(self):
         """api validates permission to delete event"""
@@ -221,10 +204,7 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete events in this category.",
-        })
+        self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
     def test_delete_event_closed_thread_no_permission(self):
         """api valdiates if user can delete events in closed threads"""
@@ -237,10 +217,9 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         self.thread.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't delete events in it.",
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't delete events in it.", status_code=403
+        )
 
     def test_delete_event_closed_category_no_permission(self):
         """api valdiates if user can delete events in closed categories"""
@@ -253,10 +232,9 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         self.category.save()
 
         response = self.client.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't delete events in it.",
-        })
+        self.assertContains(
+            response, "This category is closed. You can't delete events in it.", status_code=403
+        )
 
     def test_delete_event(self):
         """api differs posts from events"""

+ 3 - 6
misago/threads/tests/test_thread_postedits_api.py

@@ -32,7 +32,6 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor=self.user,
                 editor_name=self.user.username,
                 editor_slug=self.user.slug,
-                editor_ip='127.0.0.1',
                 edited_from="Original body",
                 edited_to="First Edit",
             ),
@@ -41,7 +40,6 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 thread=self.thread,
                 editor_name='Deleted',
                 editor_slug='deleted',
-                editor_ip='127.0.0.1',
                 edited_from="First Edit",
                 edited_to="Second Edit",
             ),
@@ -51,7 +49,6 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor=self.user,
                 editor_name=self.user.username,
                 editor_slug=self.user.slug,
-                editor_ip='127.0.0.1',
                 edited_from="Second Edit",
                 edited_to="Last Edit",
             ),
@@ -66,14 +63,14 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
 
 class ThreadPostGetEditTests(ThreadPostEditsApiTestCase):
     def test_no_edits(self):
-        """api returns 404 if post has no edits record"""
+        """api returns 403 if post has no edits record"""
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 404)
+        self.assertContains(response, "Edits record is unavailable", status_code=403)
 
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 404)
+        self.assertContains(response, "Edits record is unavailable", status_code=403)
 
     def test_empty_edit_id(self):
         """api handles empty edit in querystring"""

+ 31 - 37
misago/threads/tests/test_thread_postlikes_api.py

@@ -1,7 +1,7 @@
 from django.urls import reverse
 
-from misago.core.utils import serialize_datetime
 from misago.threads import testutils
+from misago.threads.serializers import PostLikeSerializer
 
 from .test_threads_api import ThreadsApiTestCase
 
@@ -25,20 +25,14 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
         self.override_acl({'can_see_posts_likes': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't see who liked this post.",
-        })
+        self.assertContains(response, "You can't see who liked this post.", status_code=403)
 
     def test_no_permission_to_list(self):
         """api errors if user has no permission to see likes, but can see likes count"""
         self.override_acl({'can_see_posts_likes': 1})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't see who liked this post.",
-        })
+        self.assertContains(response, "You can't see who liked this post.", status_code=403)
 
     def test_no_likes(self):
         """api returns empty list if post has no likes"""
@@ -55,22 +49,22 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.json(), [
-                {
+                PostLikeSerializer({
                     'id': other_like.id,
-                    'liked_on': serialize_datetime(other_like.liked_on),
-                    'liker_id': self.user.id,
-                    'username': self.user.username,
-                    'slug': self.user.slug,
-                    'avatars': self.user.avatars,
-                },
-                {
+                    'liked_on': other_like.liked_on,
+                    'liker_id': other_like.liker_id,
+                    'liker_name': other_like.liker_name,
+                    'liker_slug': other_like.liker_slug,
+                    'liker__avatars': self.user.avatars,
+                }).data,
+                PostLikeSerializer({
                     'id': like.id,
-                    'liked_on': serialize_datetime(like.liked_on),
-                    'liker_id': self.user.id,
-                    'username': self.user.username,
-                    'slug': self.user.slug,
-                    'avatars': self.user.avatars,
-                },
+                    'liked_on': like.liked_on,
+                    'liker_id': like.liker_id,
+                    'liker_name': like.liker_name,
+                    'liker_slug': like.liker_slug,
+                    'liker__avatars': self.user.avatars,
+                }).data,
             ]
         )
 
@@ -85,21 +79,21 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.json(), [
-                {
+                PostLikeSerializer({
                     'id': other_like.id,
-                    'liked_on': serialize_datetime(other_like.liked_on),
-                    'liker_id': None,
-                    'username': self.user.username,
-                    'slug': self.user.slug,
-                    'avatars': None,
-                },
-                {
+                    'liked_on': other_like.liked_on,
+                    'liker_id': other_like.liker_id,
+                    'liker_name': other_like.liker_name,
+                    'liker_slug': other_like.liker_slug,
+                    'liker__avatars': None,
+                }).data,
+                PostLikeSerializer({
                     'id': like.id,
-                    'liked_on': serialize_datetime(like.liked_on),
-                    'liker_id': None,
-                    'username': self.user.username,
-                    'slug': self.user.slug,
-                    'avatars': None,
-                },
+                    'liked_on': like.liked_on,
+                    'liker_id': like.liker_id,
+                    'liker_name': like.liker_name,
+                    'liker_slug': like.liker_slug,
+                    'liker__avatars': None,
+                }).data,
             ]
         )

+ 61 - 90
misago/threads/tests/test_thread_postmerge_api.py

@@ -70,58 +70,42 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({}),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't merge posts in this thread.",
-        })
+        self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
     def test_empty_data_json(self):
         """api handles empty json data"""
         response = self.client.post(
             self.api_link, json.dumps({}), content_type="application/json"
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to select at least two posts to merge."],
-        })
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_empty_data_form(self):
         """api handles empty form data"""
         response = self.client.post(self.api_link, {})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to select at least two posts to merge."],
-        })
+
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_invalid_data(self):
         """api handles post that is invalid type"""
         self.override_acl()
         response = self.client.post(self.api_link, '[]', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got list."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '123', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got int."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '"string"', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got str."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, 'malformed', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "JSON parse error - Expecting value: line 1 column 1 (char 0)",
-        })
+        self.assertContains(response, "JSON parse error", status_code=400)
 
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
@@ -132,11 +116,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to select at least two posts to merge."],
-        })
-        
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
+
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         response = self.client.post(
@@ -146,10 +129,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ['Expected a list of items but got type "str".'],
-        })
+        self.assertContains(
+            response, "Expected a list of items but got type", status_code=400
+        )
 
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
@@ -160,10 +142,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more post ids received were invalid."],
-        })
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
     def test_one_post_id(self):
         """api rejects one post id"""
@@ -174,10 +155,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to select at least two posts to merge."],
-        })
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_merge_limit(self):
         """api rejects more posts than merge limit"""
@@ -188,10 +168,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["No more than {} posts can be merged at single time.".format(POSTS_LIMIT)],
-        })
+        self.assertContains(
+            response, "No more than {} posts can be merged".format(POSTS_LIMIT), status_code=400
+        )
 
     def test_merge_event(self):
         """api recjects events"""
@@ -204,10 +183,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Events can't be merged."],
-        })
+        self.assertContains(response, "Events can't be merged.", status_code=400)
 
     def test_merge_notfound_pk(self):
         """api recjects nonexistant pk's"""
@@ -218,10 +194,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more posts to merge could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to merge could not be found.", status_code=400
+        )
 
     def test_merge_cross_threads(self):
         """api recjects attempt to merge with post made in other thread"""
@@ -235,10 +210,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more posts to merge could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to merge could not be found.", status_code=400
+        )
 
     def test_merge_authenticated_with_guest_post(self):
         """api recjects attempt to merge with post made by deleted user"""
@@ -251,10 +225,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Posts made by different users can't be merged."],
-        })
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
     def test_merge_guest_with_authenticated_post(self):
         """api recjects attempt to merge with post made by deleted user"""
@@ -267,10 +240,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Posts made by different users can't be merged."],
-        })
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
     def test_merge_guest_posts_different_usernames(self):
         """api recjects attempt to merge posts made by different guests"""
@@ -284,10 +256,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Posts made by different users can't be merged."],
-        })
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
     def test_merge_different_visibility(self):
         """api recjects attempt to merge posts with different visibility"""
@@ -303,10 +274,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Posts with different visibility can't be merged."],
-        })
+        self.assertContains(
+            response, "Posts with different visibility can't be merged.", status_code=400
+        )
 
     def test_merge_different_approval(self):
         """api recjects attempt to merge posts with different approval"""
@@ -322,10 +292,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Posts with different visibility can't be merged."],
-        })
+        self.assertContains(
+            response, "Posts with different visibility can't be merged.", status_code=400
+        )
 
     def test_closed_thread(self):
         """api validates permission to merge in closed thread"""
@@ -342,10 +311,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({'posts': posts}),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["This thread is closed. You can't merge posts in it."],
-        })
+        self.assertContains(
+            response,
+            "This thread is closed. You can't merge posts in it.",
+            status_code=400,
+        )
 
         # allow closing threads
         self.override_acl({'can_close_threads': 1})
@@ -372,10 +342,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({'posts': posts}),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["This category is closed. You can't merge posts in it."],
-        })
+        self.assertContains(
+            response,
+            "This category is closed. You can't merge posts in it.",
+            status_code=400,
+        )
 
         # allow closing threads
         self.override_acl({'can_close_threads': 1})
@@ -410,7 +381,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'posts': ["Post marked as best answer can't be merged with thread's first post."]
+            'detail': "Post marked as best answer can't be merged with thread's first post."
         })
 
     def test_merge_posts(self):

+ 74 - 118
misago/threads/tests/test_thread_postmove_api.py

@@ -95,69 +95,43 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_invalid_data(self):
         """api handles post that is invalid type"""
         self.override_acl()
         response = self.client.post(self.api_link, '[]', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got list."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '123', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got int."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '"string"', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got str."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, 'malformed', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "JSON parse error - Expecting value: line 1 column 1 (char 0)",
-        })
+        self.assertContains(response, "JSON parse error", status_code=400)
 
     def test_no_permission(self):
         """api validates permission to move"""
         self.override_acl({'can_move_posts': 0})
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't move posts in this thread.",
-        })
+        self.assertContains(response, "You can't move posts in this thread.", status_code=403)
 
     def test_move_no_new_thread_url(self):
         """api validates if new thread url was given"""
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': ["Enter link to new thread."],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(response, "Enter link to new thread.", status_code=400)
 
     def test_invalid_new_thread_url(self):
         """api validates new thread url"""
         response = self.client.post(self.api_link, {
             'new_thread': self.user.get_absolute_url(),
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': ["This is not a valid thread link."],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
     def test_current_new_thread_url(self):
         """api validates if new thread url points to current thread"""
@@ -166,11 +140,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
                 'new_thread': self.thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': ["Thread to move posts to is same as current one."],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "Thread to move posts to is same as current one.", status_code=400
+        )
 
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
@@ -183,14 +155,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, {
             'new_thread': other_new_thread,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': [
-                "The thread you have entered link to doesn't exist or you don't have permission "
-                "to see it."
-            ],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
     def test_other_thread_is_invisible(self):
         """api validates if other thread is visible"""
@@ -203,14 +170,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
                 'new_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': [
-                "The thread you have entered link to doesn't exist or you don't have permission "
-                "to see it."
-            ],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
     def test_other_thread_isnt_replyable(self):
         """api validates if other thread can be replied"""
@@ -223,22 +185,18 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
                 'new_thread': other_thread.get_absolute_url(),
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': [
-                "You can't move posts to threads you can't reply."
-            ],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "You can't move posts to threads you can't reply.", status_code=400
+        )
 
     def test_empty_data(self):
         """api handles empty data"""
+        other_thread = testutils.post_thread(self.category)
+
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_thread': ["Enter link to new thread."],
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "Enter link to new thread.", status_code=400
+        )
 
     def test_empty_posts_data_json(self):
         """api handles empty json data"""
@@ -251,10 +209,25 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to specify at least one post to move."],
-        })
+
+        self.assertContains(
+            response, "You have to specify at least one post to move.", status_code=400
+        )
+
+    def test_empty_posts_data_form(self):
+        """api handles empty form data"""
+        other_thread = testutils.post_thread(self.category)
+
+        response = self.client.post(
+            self.api_link,
+            {
+                'new_thread': other_thread.get_absolute_url(),
+            },
+        )
+
+        self.assertContains(
+            response, "You have to specify at least one post to move.", status_code=400
+        )
 
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
@@ -268,10 +241,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You have to specify at least one post to move."],
-        })
+        self.assertContains(
+            response, "You have to specify at least one post to move.", status_code=400
+        )
 
     def test_invalid_posts_data(self):
         """api handles invalid data"""
@@ -285,10 +257,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ['Expected a list of items but got type "str".'],
-        })
+        self.assertContains(
+            response, "Expected a list of items", status_code=400
+        )
 
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
@@ -302,10 +273,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more post ids received were invalid."],
-        })
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
     def test_move_limit(self):
         """api rejects more posts than move limit"""
@@ -319,10 +289,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["No more than {} posts can be moved at single time.".format(POSTS_LIMIT)],
-        })
+        self.assertContains(
+            response, "No more than {} posts can be moved".format(POSTS_LIMIT), status_code=400
+        )
 
     def test_move_invisible(self):
         """api validates posts visibility"""
@@ -336,10 +305,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more posts to move could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to move could not be found.", status_code=400
+        )
 
     def test_move_other_thread_posts(self):
         """api recjects attempt to move other thread's post"""
@@ -353,10 +321,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["One or more posts to move could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to move could not be found.", status_code=400
+        )
 
     def test_move_event(self):
         """api rejects events move"""
@@ -370,10 +337,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["Events can't be moved."],
-        })
+        self.assertContains(response, "Events can't be moved.", status_code=400)
 
     def test_move_first_post(self):
         """api rejects first post move"""
@@ -387,10 +351,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You can't move thread's first post."],
-        })
+        self.assertContains(response, "You can't move thread's first post.", status_code=400)
 
     def test_move_hidden_posts(self):
         """api recjects attempt to move urneadable hidden post"""
@@ -404,10 +365,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["You can't move posts the content you can't see."],
-        })
+        self.assertContains(
+            response, "You can't move posts the content you can't see.", status_code=400
+        )
 
     def test_move_posts_closed_thread_no_permission(self):
         """api recjects attempt to move posts from closed thread"""
@@ -426,11 +386,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        # fixme: this is bad place to return message that thread is closed
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["This thread is closed. You can't move posts in it."],
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't move posts in it.", status_code=400
+        )
 
     def test_move_posts_closed_category_no_permission(self):
         """api recjects attempt to move posts from closed thread"""
@@ -450,11 +408,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        # fixme: this is bad place to return message that category is closed
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'posts': ["This category is closed. You can't move posts in it."],
-        })
+        self.assertContains(
+            response, "This category is closed. You can't move posts in it.", status_code=400
+        )
 
     def test_move_posts(self):
         """api moves posts to other thread"""
@@ -530,10 +486,10 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        posts = [
+        posts = (
             testutils.reply_thread(self.thread),
             testutils.reply_thread(self.thread),
-        ]
+        )
 
         self.refresh_thread()
         self.assertEqual(self.thread.replies, 2)

+ 176 - 145
misago/threads/tests/test_thread_postpatch_api.py

@@ -27,7 +27,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
             kwargs={
                 'thread_pk': self.thread.pk,
                 'pk': self.post.pk,
-            },
+            }
         )
 
     def patch(self, api_link, ops):
@@ -101,8 +101,8 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['is_protected'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['is_protected'])
 
         self.refresh_post()
         self.assertTrue(self.post.is_protected)
@@ -125,8 +125,8 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_protected'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_protected'])
 
         self.refresh_post()
         self.assertFalse(self.post.is_protected)
@@ -151,8 +151,8 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['is_protected'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['is_protected'])
 
         self.refresh_post()
         self.assertTrue(self.post.is_protected)
@@ -183,8 +183,8 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_protected'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_protected'])
 
         self.refresh_post()
         self.assertFalse(self.post.is_protected)
@@ -205,10 +205,10 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't protect posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't protect posts in this category.")
 
         self.refresh_post()
         self.assertFalse(self.post.is_protected)
@@ -229,10 +229,10 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't protect posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't protect posts in this category.")
 
         self.refresh_post()
         self.assertTrue(self.post.is_protected)
@@ -250,10 +250,10 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't protect posts you can't edit.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't protect posts you can't edit.")
 
         self.refresh_post()
         self.assertFalse(self.post.is_protected)
@@ -274,10 +274,10 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't protect posts you can't edit.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't protect posts you can't edit.")
 
         self.refresh_post()
         self.assertTrue(self.post.is_protected)
@@ -302,8 +302,8 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_unapproved'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_unapproved'])
 
         self.refresh_post()
         self.assertFalse(self.post.is_unapproved)
@@ -321,10 +321,10 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Content approval can't be reversed.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
 
         self.refresh_post()
         self.assertFalse(self.post.is_unapproved)
@@ -345,10 +345,10 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't approve posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't approve posts in this category.")
 
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
@@ -375,10 +375,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't approve posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0],
+            "This thread is closed. You can't approve posts in it.",
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
@@ -405,10 +408,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't approve posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0],
+            "This category is closed. You can't approve posts in it.",
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
@@ -432,10 +438,10 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't approve thread's first post.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't approve thread's first post.")
 
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
@@ -457,10 +463,12 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't approve posts the content you can't see.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't approve posts the content you can't see."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
@@ -482,8 +490,8 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['is_hidden'])
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -503,8 +511,8 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertTrue(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertTrue(reponse_json['is_hidden'])
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -522,10 +530,10 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't hide posts in this category.")
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -546,10 +554,10 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This post is protected. You can't hide it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "This post is protected. You can't hide it.")
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -570,10 +578,12 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide other users posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide other users posts in this category."
+        )
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -594,10 +604,12 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide posts that are older than 1 minute.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide posts that are older than 1 minute."
+        )
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -618,10 +630,12 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't hide posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't hide posts in it."
+        )
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -642,10 +656,12 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't hide posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't hide posts in it."
+        )
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -666,10 +682,10 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide thread's first post.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
 
     def test_hide_best_answer(self):
         """api hide first post fails"""
@@ -687,9 +703,10 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'detail': "You can't hide this post because its marked as best answer.",
+            'id': self.post.id,
+            'detail': ["You can't hide this post because its marked as best answer."],
         })
 
 
@@ -715,8 +732,8 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_hidden'])
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -742,8 +759,8 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
         )
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertFalse(response_json['is_hidden'])
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_hidden'])
 
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
@@ -767,10 +784,10 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reveal posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't reveal posts in this category.")
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -794,10 +811,12 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This post is protected. You can't reveal it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This post is protected. You can't reveal it."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -819,10 +838,12 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reveal other users posts in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't reveal other users posts in this category."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -844,10 +865,12 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reveal posts that are older than 1 minute.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't reveal posts that are older than 1 minute."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -871,10 +894,12 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't reveal posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't reveal posts in it."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -898,10 +923,12 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't reveal posts in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't reveal posts in it."
+        )
 
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
@@ -922,10 +949,10 @@ class PostUnhideApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reveal thread's first post.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't reveal thread's first post.")
 
 
 class PostLikeApiTests(ThreadPostPatchApiTestCase):
@@ -942,10 +969,7 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't like posts in this category.",
-        })
+        self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
     def test_like_no_like_permission(self):
         """api validates user's permission to see posts likes"""
@@ -960,10 +984,7 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't like posts in this category.",
-        })
+        self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
     def test_like_post(self):
         """api adds user like to post"""
@@ -1242,10 +1263,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't hide events in this category.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide events in this category."
+        )
 
         self.refresh_event()
         self.assertFalse(self.event.is_hidden)
@@ -1269,10 +1292,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't hide events in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't hide events in it."
+        )
 
         self.refresh_event()
         self.assertFalse(self.event.is_hidden)
@@ -1296,10 +1321,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't hide events in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't hide events in it."
+        )
 
         self.refresh_event()
         self.assertFalse(self.event.is_hidden)
@@ -1347,10 +1374,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This thread is closed. You can't reveal events in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't reveal events in it."
+        )
 
         self.refresh_event()
         self.assertTrue(self.event.is_hidden)
@@ -1377,10 +1406,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
                 },
             ]
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't reveal events in it.",
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't reveal events in it."
+        )
 
         self.refresh_event()
         self.assertTrue(self.event.is_hidden)

+ 7 - 10
misago/threads/tests/test_thread_postread_api.py

@@ -29,22 +29,19 @@ class PostReadApiTests(ThreadsApiTestCase):
         self.logout_user()
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_read_post(self):
         """api marks post as read"""
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'thread_is_read': False,
-        })
 
         self.assertEqual(self.user.postread_set.count(), 1)
         self.user.postread_set.get(post=self.post)
 
+        # one post read, first post is still unread
+        self.assertFalse(response.json()['thread_is_read'])
+
         # read second post
         response = self.client.post(reverse(
             'misago:api:thread-post-read',
@@ -54,13 +51,13 @@ class PostReadApiTests(ThreadsApiTestCase):
             }
         ))
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'thread_is_read': True,
-        })
 
         self.assertEqual(self.user.postread_set.count(), 2)
         self.user.postread_set.get(post=self.thread.first_post)
 
+        # both posts are read
+        self.assertTrue(response.json()['thread_is_read'])
+
     def test_read_subscribed_thread_post(self):
         """api marks post as read and updates subscription"""
         self.thread.subscription_set.create(

+ 40 - 101
misago/threads/tests/test_thread_postsplit_api.py

@@ -99,59 +99,38 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_no_permission(self):
         """api validates permission to split"""
         self.override_acl({'can_move_posts': 0})
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't split posts from this thread.",
-        })
+        self.assertContains(response, "You can't split posts from this thread.", status_code=403)
 
     def test_empty_data(self):
         """api handles empty data"""
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["You have to specify at least one post to split."],
-        })
+        self.assertContains(
+            response, "You have to specify at least one post to split.", status_code=400
+        )
 
     def test_invalid_data(self):
         """api handles post that is invalid type"""
         self.override_acl()
         response = self.client.post(self.api_link, '[]', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got list."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '123', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got int."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, '"string"', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got str."],
-        })
+        self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
 
         self.override_acl()
         response = self.client.post(self.api_link, 'malformed', content_type="application/json")
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "JSON parse error - Expecting value: line 1 column 1 (char 0)",
-        })
+        self.assertContains(response, "JSON parse error", status_code=400)
 
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
@@ -160,12 +139,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             json.dumps({}),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["You have to specify at least one post to split."],
-        })
+        self.assertContains(
+            response, "You have to specify at least one post to split.", status_code=400
+        )
 
     def test_empty_posts_ids(self):
         """api rejects empty posts ids list"""
@@ -176,12 +152,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["You have to specify at least one post to split."],
-        })
+        self.assertContains(
+            response, "You have to specify at least one post to split.", status_code=400
+        )
 
     def test_invalid_posts_data(self):
         """api handles invalid data"""
@@ -192,12 +165,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ['Expected a list of items but got type "str".'],
-        })
+        self.assertContains(
+            response, "Expected a list of items but got type", status_code=400
+        )
 
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
@@ -208,12 +178,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["One or more post ids received were invalid."],
-        })
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
     def test_split_limit(self):
         """api rejects more posts than split limit"""
@@ -224,12 +191,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["No more than {} posts can be split at single time.".format(POSTS_LIMIT)],
-        })
+        self.assertContains(
+            response, "No more than {} posts can be split".format(POSTS_LIMIT), status_code=400
+        )
 
     def test_split_invisible(self):
         """api validates posts visibility"""
@@ -240,12 +204,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["One or more posts to split could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to split could not be found.", status_code=400
+        )
 
     def test_split_event(self):
         """api rejects events split"""
@@ -256,12 +217,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["Events can't be split."],
-        })
+        self.assertContains(response, "Events can't be split.", status_code=400)
 
     def test_split_first_post(self):
         """api rejects first post split"""
@@ -272,12 +228,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["You can't split thread's first post."],
-        })
+        self.assertContains(response, "You can't split thread's first post.", status_code=400)
 
     def test_split_hidden_posts(self):
         """api recjects attempt to split urneadable hidden post"""
@@ -288,12 +239,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["You can't split posts the content you can't see."],
-        })
+        self.assertContains(
+            response, "You can't split posts the content you can't see.", status_code=400
+        )
 
     def test_split_posts_closed_thread_no_permission(self):
         """api recjects attempt to split posts from closed thread"""
@@ -309,12 +257,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["This thread is closed. You can't split posts in it."],
-        })
+        self.assertContains(
+            response, "This thread is closed. You can't split posts in it.", status_code=400
+        )
 
     def test_split_posts_closed_category_no_permission(self):
         """api recjects attempt to split posts from closed thread"""
@@ -330,12 +275,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["This category is closed. You can't split posts in it."],
-        })
+        self.assertContains(
+            response, "This category is closed. You can't split posts in it.", status_code=400
+        )
 
     def test_split_other_thread_posts(self):
         """api recjects attempt to split other thread's post"""
@@ -348,12 +290,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': ["This field is required."],
-            'category': ["This field is required."],
-            'posts': ["One or more posts to split could not be found."],
-        })
+        self.assertContains(
+            response, "One or more posts to split could not be found.", status_code=400
+        )
 
     def test_split_empty_new_thread_data(self):
         """api handles empty form data"""

+ 19 - 28
misago/threads/tests/test_thread_reply_api.py

@@ -43,36 +43,29 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_thread_visibility(self):
         """thread's visibility is validated"""
         self.override_acl({'can_see': 0})
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_browse': 0})
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_see_all_threads': 0})
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_cant_reply_thread(self):
         """permission to reply thread is validated"""
         self.override_acl({'can_reply_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reply to threads in this category.",
-        })
+        self.assertContains(
+            response, "You can't reply to threads in this category.", status_code=403
+        )
 
     def test_closed_category(self):
         """permssion to reply in closed category is validated"""
@@ -82,10 +75,11 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.category.save()
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This category is closed. You can't reply to threads in it.",
-        })
+        self.assertContains(
+            response,
+            "This category is closed. You can't reply to threads in it.",
+            status_code=403
+        )
 
         # allow to post in closed category
         self.override_acl({'can_close_threads': 1})
@@ -101,10 +95,9 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.thread.save()
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't reply to closed threads in this category.",
-        })
+        self.assertContains(
+            response, "You can't reply to closed threads in this category.", status_code=403
+        )
 
         # allow to post in closed thread
         self.override_acl({'can_close_threads': 1})
@@ -117,11 +110,9 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.override_acl()
 
         response = self.client.post(self.api_link, data={})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': ['You have to enter a message.'],
-        })
-        
+
+        self.assertContains(response, "You have to enter a message.", status_code=400)
+
     def test_invalid_data(self):
         """api errors for invalid request data"""
         self.override_acl()
@@ -131,10 +122,8 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
+
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_post_is_validated(self):
         """post is validated"""
@@ -162,7 +151,7 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-        
+
         thread = Thread.objects.get(pk=self.thread.pk)
 
         self.override_acl()
@@ -174,6 +163,8 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.assertEqual(self.user.threads, 0)
         self.assertEqual(self.user.posts, 1)
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
         post = self.user.post_set.all()[:1][0]
         self.assertEqual(post.category_id, self.category.pk)
         self.assertEqual(post.original, "This is test response!")

+ 21 - 46
misago/threads/tests/test_thread_start_api.py

@@ -45,9 +45,6 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
 
     def test_cant_see(self):
         """has no permission to see selected category"""
@@ -56,13 +53,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, {
             'category': self.category.pk,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'category': ["Selected category is invalid."],
-            'title': ["You have to enter thread title."],
-            'post': ["You have to enter a message."],
-        })
-        
+
+        self.assertContains(response, "Selected category is invalid.", status_code=400)
+
     def test_cant_browse(self):
         """has no permission to browse selected category"""
         self.override_acl({'can_browse': 0})
@@ -70,12 +63,8 @@ class StartThreadTests(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, {
             'category': self.category.pk,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'category': ["Selected category is invalid."],
-            'title': ["You have to enter thread title."],
-            'post': ["You have to enter a message."],
-        })
+
+        self.assertContains(response, "Selected category is invalid.", status_code=400)
 
     def test_cant_start_thread(self):
         """permission to start thread in category is validated"""
@@ -84,14 +73,12 @@ class StartThreadTests(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, {
             'category': self.category.pk,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'category': ["You don't have permission to start new threads in this category."],
-            'title': ["You have to enter thread title."],
-            'post': ["You have to enter a message."],
-        })
 
-    def test_cant_start_thread_in_closed_category(self):
+        self.assertContains(
+            response, "You don't have permission to start new threads", status_code=400
+        )
+
+    def test_cant_start_thread_in_locked_category(self):
         """can't post in closed category"""
         self.category.is_closed = True
         self.category.save()
@@ -101,12 +88,8 @@ class StartThreadTests(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, {
             'category': self.category.pk,
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'category': ["This category is closed. You can't start new threads in it."],
-            'title': ["You have to enter thread title."],
-            'post': ["You have to enter a message."],
-        })
+
+        self.assertContains(response, "This category is closed.", status_code=400)
 
     def test_cant_start_thread_in_invalid_category(self):
         """can't post in invalid category"""
@@ -115,16 +98,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         self.override_acl({'can_close_threads': 0})
 
-        response = self.client.post(self.api_link, {'category': self.category.pk * 100})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            # fixme: invalid category should return same error as invisible one
-            'category': [
-                "Selected category doesn't exist or you don't have permission to browse it."
-            ],
-            'title': ["You have to enter thread title."],
-            'post': ["You have to enter a message."],
-        })
+        response = self.client.post(self.api_link, {'category': self.category.pk * 100000})
+
+        self.assertContains(response, "Selected category doesn't exist", status_code=400)
 
     def test_empty_data(self):
         """no data sent handling has no showstoppers"""
@@ -149,12 +125,8 @@ class StartThreadTests(AuthenticatedUserTestCase):
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-            }
-        )
+
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_title_is_validated(self):
         """title is validated"""
@@ -168,6 +140,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'post': "Lorem ipsum dolor met, sit amet elit!",
             }
         )
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
@@ -187,6 +160,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'post': "a",
             }
         )
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
@@ -210,7 +184,6 @@ class StartThreadTests(AuthenticatedUserTestCase):
         thread = self.user.thread_set.all()[:1][0]
 
         response_json = response.json()
-        self.assertEqual(response_json['title'], "Hello, I am test thread!")
         self.assertEqual(response_json['url'], thread.get_absolute_url())
 
         self.override_acl()
@@ -224,6 +197,8 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.assertEqual(self.user.threads, 1)
         self.assertEqual(self.user.posts, 1)
 
+        self.assertEqual(self.user.audittrail_set.count(), 1)
+
         self.assertEqual(thread.category_id, self.category.pk)
         self.assertEqual(thread.title, "Hello, I am test thread!")
         self.assertEqual(thread.starter_id, self.user.id)

+ 0 - 1
misago/threads/tests/test_threadparticipant_model.py

@@ -31,7 +31,6 @@ class ThreadParticipantTests(TestCase):
             category=self.category,
             thread=self.thread,
             poster_name='Tester',
-            poster_ip='127.0.0.1',
             original="Hello! I am test message!",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",

+ 7 - 4
misago/threads/tests/test_threads_api.py

@@ -4,7 +4,8 @@ from django.utils import timezone
 from django.urls import reverse
 
 from misago.acl.testutils import override_acl
-from misago.categories.models import THREADS_ROOT, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads.models import Thread
 from misago.threads.threadtypes import trees_map
@@ -15,7 +16,7 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
     def setUp(self):
         super(ThreadsApiTestCase, self).setUp()
 
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
 
         self.root = Category.objects.get(tree_id=threads_tree_id, level=0)
         self.category = Category.objects.get(slug='first-category')
@@ -148,20 +149,22 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
             response, hidden_post.parsed
         )  # hidden post's body is visible with permission
 
+        self.override_acl({'can_approve_content': 0})
+
         # unapproved posts shouldn't show at all
         unapproved_post = testutils.reply_thread(
             self.thread,
             is_unapproved=True,
         )
 
-        self.override_acl({'can_approve_content': 0})
         response = self.client.get(self.tested_links[1])
         self.assertNotContains(response, unapproved_post.get_absolute_url())
 
         # add permission to see unapproved posts
         self.override_acl({'can_approve_content': 1})
+
         response = self.client.get(self.tested_links[1])
-        self.assertContains(response, unapproved_post.parsed)
+        self.assertContains(response, unapproved_post.get_absolute_url())
 
     def test_api_validates_has_unapproved_posts_visibility(self):
         """api checks acl before exposing unapproved flag"""

+ 47 - 88
misago/threads/tests/test_threads_bulkdelete_api.py

@@ -1,10 +1,10 @@
 import json
 
 from django.urls import reverse
-from django.utils import six
 
 from misago.acl.testutils import override_acl
-from misago.categories.models import PRIVATE_THREADS_ROOT, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads.models import Thread
 from misago.threads.serializers.moderation import THREADS_LIMIT
@@ -39,10 +39,7 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         self.logout_user()
 
         response = self.delete(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
 
     def test_delete_no_ids(self):
         """api requires ids to delete"""
@@ -52,10 +49,7 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': ["You have to specify at least one thread to delete."],
-        })
+        self.assertContains(response, "You have to specify at least one thread to delete.", status_code=403)
 
     def test_validate_ids(self):
         """api validates that ids are list of ints"""
@@ -65,34 +59,13 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, True)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': ['Expected a list of items but got type "bool".'],
-        })
-
-        self.override_acl({
-            'can_hide_own_threads': 2,
-            'can_hide_threads': 2,
-        })
+        self.assertContains(response, "Expected a list of items", status_code=403)
 
         response = self.delete(self.api_link, 'abbss')
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': [
-                'Expected a list of items but got type "{}".'.format(six.text_type.__name__)
-            ],
-        })
-        
-        self.override_acl({
-            'can_hide_own_threads': 2,
-            'can_hide_threads': 2,
-        })
+        self.assertContains(response, "Expected a list of items", status_code=403)
 
         response = self.delete(self.api_link, [1, 2, 3, 'a', 'b', 'x'])
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': ["One or more thread ids received were invalid."],
-        })
+        self.assertContains(response, "One or more thread ids received were invalid.", status_code=403)
 
     def test_validate_ids_length(self):
         """api validates that ids are list of ints"""
@@ -102,12 +75,11 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, list(range(THREADS_LIMIT + 1)))
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': [
-                "No more than {} threads can be deleted at single time.".format(THREADS_LIMIT)
-            ],
-        })
+        self.assertContains(
+            response,
+            "No more than {} threads can be deleted at single time.".format(THREADS_LIMIT),
+            status_code=403,
+        )
 
     def test_validate_thread_visibility(self):
         """api valdiates if user can see deleted thread"""
@@ -124,10 +96,7 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         threads_ids = [p.id for p in self.threads]
 
         response = self.delete(self.api_link, threads_ids)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': ["One or more threads to delete could not be found."],
-        })
+        self.assertContains(response, "threads to delete could not be found", status_code=403)
 
         # no thread was deleted
         for thread in self.threads:
@@ -143,20 +112,17 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         other_thread = self.threads[1]
 
         response = self.delete(self.api_link, [p.id for p in self.threads])
+
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': {
-                'details': [
-                    {
-                        'thread': {
-                            'id': str(other_thread.pk),
-                            'title': other_thread.title
-                        },
-                        'error': "You can't delete other users theads in this category.",
-                    },
-                ],
-            },
-        })
+        self.assertEqual(response.json(), [
+            {
+                'thread': {
+                    'id': other_thread.pk,
+                    'title': other_thread.title
+                },
+                'error': "You can't delete other users theads in this category."
+            }
+        ])
 
         # no threads are removed on failed attempt
         for thread in self.threads:
@@ -174,20 +140,17 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, [p.id for p in self.threads])
+
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': {
-                'details': [
-                    {
-                        'thread': {
-                            'id': str(thread.pk),
-                            'title': thread.title,
-                        },
-                        'error': "This category is closed. You can't delete threads in it.",
-                    } for thread in reversed(self.threads)
-                ],
-            },
-        })
+        self.assertEqual(response.json(), [
+            {
+                'thread': {
+                    'id': thread.pk,
+                    'title': thread.title
+                },
+                'error': "This category is closed. You can't delete threads in it."
+            } for thread in sorted(self.threads, key=lambda i: i.pk)
+        ])
 
     def test_delete_thread_closed_no_permission(self):
         """api tests thread's closed state"""
@@ -202,27 +165,24 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         })
 
         response = self.delete(self.api_link, [p.id for p in self.threads])
+
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': {
-                'details': [
-                    {
-                        'thread': {
-                            'id': str(closed_thread.pk),
-                            'title': closed_thread.title,
-                        },
-                        'error': "This thread is closed. You can't delete it.",
-                    },
-                ],
-            },
-        })
+        self.assertEqual(response.json(), [
+            {
+                'thread': {
+                    'id': closed_thread.pk,
+                    'title': closed_thread.title
+                },
+                'error': "This thread is closed. You can't delete it."
+            }
+        ])
 
     def test_delete_private_thread(self):
         """attempt to delete private thread fails"""
         private_thread = self.threads[0]
 
         private_thread.category = Category.objects.get(
-            tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT),
+            tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME),
         )
         private_thread.save()
 
@@ -239,9 +199,8 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
         threads_ids = [p.id for p in self.threads]
 
         response = self.delete(self.api_link, threads_ids)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'threads': ["One or more threads to delete could not be found."],
-        })
+
+        self.assertEqual(response.status_code, 403)
+        self.assertContains(response, "threads to delete could not be found", status_code=403)
 
         Thread.objects.get(pk=private_thread.pk)

+ 34 - 118
misago/threads/tests/test_threads_editor_api.py

@@ -80,40 +80,21 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You need to be signed in to start threads.",
-            }
-        )
+        self.assertContains(response, "You need to be signed in", status_code=403)
 
     def test_category_visibility_validation(self):
         """endpoint omits non-browseable categories"""
         self.override_acl({'can_browse': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': (
-                    "No categories that allow new threads are available to you at the moment."
-                ),
-            }
-        )
+        self.assertContains(response, "No categories that allow new threads", status_code=403)
 
     def test_category_disallowing_new_threads(self):
         """endpoint omits category disallowing starting threads"""
         self.override_acl({'can_start_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': (
-                    "No categories that allow new threads are available to you at the moment."
-                ),
-            }
-        )
+        self.assertContains(response, "No categories that allow new threads", status_code=403)
 
     def test_category_closed_disallowing_new_threads(self):
         """endpoint omits closed category"""
@@ -123,14 +104,7 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.category.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': (
-                    "No categories that allow new threads are available to you at the moment."
-                ),
-            }
-        )
+        self.assertContains(response, "No categories that allow new threads", status_code=403)
 
     def test_category_closed_allowing_new_threads(self):
         """endpoint adds closed category that allows new threads"""
@@ -297,40 +271,29 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You have to sign in to reply threads.",
-            }
-        )
+        self.assertContains(response, "You have to sign in to reply threads.", status_code=403)
 
     def test_thread_visibility(self):
         """thread's visibility is validated"""
         self.override_acl({'can_see': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_browse': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_see_all_threads': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_no_reply_permission(self):
         """permssion to reply is validated"""
         self.override_acl({'can_reply_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't reply to threads in this category.",
-            }
+        self.assertContains(
+            response, "You can't reply to threads in this category.", status_code=403
         )
 
     def test_closed_category(self):
@@ -341,11 +304,10 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         self.category.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This category is closed. You can't reply to threads in it.",
-            }
+        self.assertContains(
+            response,
+            "This category is closed. You can't reply to threads in it.",
+            status_code=403
         )
 
         # allow to post in closed category
@@ -362,11 +324,8 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         self.thread.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't reply to closed threads in this category.",
-            }
+        self.assertContains(
+            response, "You can't reply to closed threads in this category.", status_code=403
         )
 
         # allow to post in closed thread
@@ -382,16 +341,18 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-    def test_reply_to_invisible_post(self):
+    def test_reply_to_visibility(self):
         """api validates replied post visibility"""
         self.override_acl({'can_reply_threads': 1})
 
         # unapproved reply can't be replied to
-        unapproved_reply = testutils.reply_thread(self.thread, is_unapproved=True)
+        unapproved_reply = testutils.reply_thread(
+            self.thread,
+            is_unapproved=True,
+        )
 
         response = self.client.get('{}?reply={}'.format(self.api_link, unapproved_reply.pk))
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         # hidden reply can't be replied to
         self.override_acl({'can_reply_threads': 1})
@@ -399,12 +360,7 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         hidden_reply = testutils.reply_thread(self.thread, is_hidden=True)
 
         response = self.client.get('{}?reply={}'.format(self.api_link, hidden_reply.pk))
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't reply to hidden posts.",
-            }
-        )
+        self.assertContains(response, "You can't reply to hidden posts", status_code=403)
 
     def test_reply_to_other_thread_post(self):
         """api validates is replied post belongs to same thread"""
@@ -413,7 +369,6 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
         response = self.client.get('{}?reply={}'.format(self.api_link, reply_to.pk))
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_reply_to_event(self):
         """events can't be edited"""
@@ -422,12 +377,8 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         reply_to = testutils.reply_thread(self.thread, is_event=True)
 
         response = self.client.get('{}?reply={}'.format(self.api_link, reply_to.pk))
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't reply to events.",
-            }
-        )
+
+        self.assertContains(response, "You can't reply to events.", status_code=403)
 
     def test_reply_to(self):
         """api includes replied to post details in response"""
@@ -467,41 +418,28 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You have to sign in to edit posts.",
-            }
-        )
+        self.assertContains(response, "You have to sign in to edit posts.", status_code=403)
 
     def test_thread_visibility(self):
         """thread's visibility is validated"""
         self.override_acl({'can_see': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_browse': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         self.override_acl({'can_see_all_threads': 0})
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_no_edit_permission(self):
         """permssion to edit is validated"""
         self.override_acl({'can_edit_posts': 0})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't edit posts in this category.",
-            }
-        )
+        self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
     def test_closed_category(self):
         """permssion to edit in closed category is validated"""
@@ -511,11 +449,8 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.category.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This category is closed. You can't edit posts in it.",
-            }
+        self.assertContains(
+            response, "This category is closed. You can't edit posts in it.", status_code=403
         )
 
         # allow to edit in closed category
@@ -532,11 +467,8 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.thread.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This thread is closed. You can't edit posts in it.",
-            }
+        self.assertContains(
+            response, "This thread is closed. You can't edit posts in it.", status_code=403
         )
 
         # allow to edit in closed thread
@@ -553,11 +485,8 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.post.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This post is protected. You can't edit it.",
-            }
+        self.assertContains(
+            response, "This post is protected. You can't edit it.", status_code=403
         )
 
         # allow to post in closed thread
@@ -574,12 +503,7 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.post.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "This post is hidden, you can't edit it.",
-            }
-        )
+        self.assertContains(response, "This post is hidden, you can't edit it.", status_code=403)
 
         # allow hidden edition
         self.override_acl({'can_edit_posts': 1, 'can_hide_posts': 1})
@@ -599,7 +523,6 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
         # allow unapproved edition
         self.override_acl({'can_edit_posts': 2, 'can_approve_content': 1})
@@ -615,12 +538,8 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.post.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "Events can't be edited.",
-            }
-        )
+
+        self.assertContains(response, "Events can't be edited.", status_code=403)
 
     def test_other_user_post(self):
         """api validates if other user's post can be edited"""
@@ -630,11 +549,8 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.post.save()
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.json(), {
-                'detail': "You can't edit other users posts in this category.",
-            }
+        self.assertContains(
+            response, "You can't edit other users posts in this category.", status_code=403
         )
 
         # allow other users post edition

+ 108 - 175
misago/threads/tests/test_threads_merge_api.py

@@ -1,6 +1,5 @@
 import json
 
-from django.utils import six
 from django.urls import reverse
 
 from misago.acl import add_acl
@@ -31,7 +30,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.category_b = Category.objects.get(slug='category-b')
 
     def override_other_category(self):
-        categories = self.user.acl_cache['categories']
+        categories =  self.user.acl_cache['categories']
 
         visible_categories = self.user.acl_cache['visible_categories']
         browseable_categories = self.user.acl_cache['browseable_categories']
@@ -56,15 +55,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
-    def test_empty_data(self):
+    def test_merge_no_threads(self):
         """api validates if we are trying to merge no threads"""
         response = self.client.post(self.api_link, content_type="application/json")
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
-                'title': ['This field is required.'],
-                'category': ['This field is required.'],
-                'threads': ["You have to select at least two threads to merge."],
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
             }
         )
 
@@ -73,16 +72,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': [],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
-                'threads': ["You have to select at least two threads to merge."],
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
             }
         )
 
@@ -91,34 +90,25 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': 'abcd',
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'threads': [
-                    'Expected a list of items but got type "{}".'.format(six.text_type.__name__)
-                ],
-            }
-        )
+        self.assertContains(response, "Expected a list of items", status_code=403)
 
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': ['a', '-', 'c'],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
-                'threads': ["One or more thread ids received were invalid."],
+            response_json, {
+                'detail': "One or more thread ids received were invalid.",
             }
         )
 
@@ -127,82 +117,55 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
-                'threads': ["You have to select at least two threads to merge."],
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
             }
         )
 
     def test_merge_with_nonexisting_thread(self):
         """api validates if we are trying to merge with invalid thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
-
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, self.thread.id + 1000],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(),
-            {
-                'merge': [
-                    {
-                        'id': str(self.thread.id + 1000),
-                        'status': '404',
-                        'detail': (
-                            "Requested thread doesn't exist or you "
-                            "don't have permission to see it."
-                        )
-                    },
-                ],
-            },
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
         )
 
     def test_merge_with_invisible_thread(self):
         """api validates if we are trying to merge with inaccesible thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
         unaccesible_thread = testutils.post_thread(category=self.category_b)
 
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, unaccesible_thread.id],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(),
-            {
-                'merge': [
-                    {
-                        'id': str(unaccesible_thread.id),
-                        'status': '404',
-                        'detail': (
-                            "Requested thread doesn't exist or you "
-                            "don't have permission to see it."
-                        )
-                    },
-                ],
-            },
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
         )
 
     def test_merge_no_permission(self):
@@ -218,59 +181,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'merge': [
-                    {
-                        'id': str(self.thread.pk),
-                        'status': '403',
-                        'detail': "You can't merge threads in this category.",
-                    },
-                    {
-                        'id': str(thread.pk),
-                        'status': '403',
-                        'detail': "You can't merge threads in this category.",
-                    },
-                ],
-            }
-        )
-
-    def test_merge_no_permission_with_invisible_thread(self):
-        """api validates if we are trying to merge with inaccesible thread without permission"""
-        unaccesible_thread = testutils.post_thread(category=self.category_b)
+        self.assertEqual(response.status_code, 403)
 
-        response = self.client.post(
-            self.api_link,
-            json.dumps({
-                'category': self.category.pk,
-                'title': 'Lorem ipsum dolor',
-                'threads': [self.thread.id, unaccesible_thread.id],
-            }),
-            content_type="application/json",
-        )
-        self.assertEqual(response.status_code, 400)
+        response_json = response.json()
         self.assertEqual(
-            response.json(),
-            {
-                'merge': [
-                    {
-                        'id': str(self.thread.id),
-                        'status': '403',
-                        'detail': (
-                            "You can't merge threads in this category."
-                        )
-                    },
-                    {
-                        'id': str(unaccesible_thread.id),
-                        'status': '404',
-                        'detail': (
-                            "Requested thread doesn't exist or you "
-                            "don't have permission to see it."
-                        )
-                    },
-                ],
-            },
+            response_json, [
+                {
+                    'id': thread.pk,
+                    'title': thread.title,
+                    'errors': ["You can't merge threads in this category."],
+                },
+                {
+                    'id': self.thread.pk,
+                    'title': self.thread.title,
+                    'errors': ["You can't merge threads in this category."],
+                },
+            ]
         )
 
     def test_thread_category_is_closed(self):
@@ -295,22 +221,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'merge': [
-                    {
-                        'status': '403',
-                        'id': str(self.thread.pk),
-                        'detail': "This category is closed. You can't merge it's threads.",
-                    },
-                    {
-                        'status': '403',
-                        'id': str(other_thread.pk),
-                        'detail': "This category is closed. You can't merge it's threads.",
-                    },
-                ],
-            }
+        self.assertContains(
+            response,
+            "This category is closed. You can't merge it's threads.",
+            status_code=403,
         )
 
     def test_thread_is_closed(self):
@@ -319,6 +233,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_merge_threads': 1,
             'can_close_threads': 0,
         })
+        self.override_other_category()
 
         other_thread = testutils.post_thread(self.category)
 
@@ -328,23 +243,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
+                'category': self.category_b.pk,
                 'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, other_thread.id],
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'merge': [
-                    {
-                        'id': str(other_thread.pk),
-                        'status': '403',
-                        'detail': "This thread is closed. You can't merge it with other threads.",
-                    },
-                ],
-            }
+        self.assertContains(
+            response,
+            "This thread is closed. You can't merge it with other threads.",
+            status_code=403,
         )
 
     def test_merge_too_many_threads(self):
@@ -359,23 +267,20 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_edit_threads': False,
             'can_reply_threads': False,
         })
-        self.override_other_category()
 
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category_b.pk,
-                'title': 'Lorem ipsum dolor',
                 'threads': threads,
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
-                'threads': [
-                    "No more than %s threads can be merged at single time." % THREADS_LIMIT
-                ],
+            response_json, {
+                'detail': "No more than %s threads can be merged at single time." % THREADS_LIMIT,
             }
         )
 
@@ -398,8 +303,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ['This field is required.'],
                 'category': ['This field is required.'],
             }
@@ -426,8 +333,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ["Thread title should be at least 5 characters long (it has 3)."],
             }
         )
@@ -453,8 +362,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'category': ["Requested category could not be found."],
             }
         )
@@ -481,8 +392,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'category': ["You can't create new threads in selected category."],
             }
         )
@@ -509,8 +422,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'weight': ["Ensure this value is less than or equal to 2."],
             }
         )
@@ -537,8 +452,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'weight': ["You don't have permission to pin threads globally in this category."],
             }
         )
@@ -565,8 +482,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'weight': ["You don't have permission to pin threads in this category."],
             }
         )
@@ -594,8 +513,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ["Thread title should be at least 5 characters long (it has 3)."],
             }
         )
@@ -623,8 +544,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ["Thread title should be at least 5 characters long (it has 3)."],
             }
         )
@@ -651,8 +574,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'is_closed': ["You don't have permission to close threads in this category."],
             }
         )
@@ -680,8 +605,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ["Thread title should be at least 5 characters long (it has 3)."],
             }
         )
@@ -709,8 +636,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'is_hidden': ["You don't have permission to hide threads in this category."],
             }
         )
@@ -739,8 +668,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
         self.assertEqual(
-            response.json(), {
+            response_json, {
                 'title': ["Thread title should be at least 5 characters long (it has 3)."],
             }
         )
@@ -770,14 +701,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         # is response json with new thread?
-        new_thread = Thread.objects.get(pk=response.json()['id'])
+        response_json = response.json()
+
+        new_thread = Thread.objects.get(pk=response_json['id'])
         new_thread.is_read = False
         new_thread.subscription = None
 
         add_acl(self.user, new_thread.category)
         add_acl(self.user, new_thread)
 
-        self.assertEqual(response.json(), ThreadsListSerializer(new_thread).data)
+        self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
 
         # did posts move to new thread?
         for post in Post.objects.filter(id__in=posts_ids):
@@ -1119,7 +1052,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             self.api_link,
             json.dumps({
                 'threads': [self.thread.id, other_thread.id],
-                'title': "Merged thread!",
+                'title': 'Merged thread!',
                 'category': self.category.id,
             }),
             content_type="application/json",

+ 232 - 59
misago/threads/tests/test_threadslists.py

@@ -5,7 +5,6 @@ from django.utils import timezone
 from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
-from misago.api.testutils import ApiTestsMixin
 from misago.categories.models import Category
 from misago.conf import settings
 from misago.readtracker import poststracker
@@ -17,7 +16,7 @@ from misago.users.testutils import AuthenticatedUserTestCase
 LISTS_URLS = ('', 'my/', 'new/', 'unread/', 'subscribed/', )
 
 
-class ThreadsListTestCase(AuthenticatedUserTestCase, ApiTestsMixin):
+class ThreadsListTestCase(AuthenticatedUserTestCase):
     def setUp(self):
         """
         Create categories tree for test cases:
@@ -213,7 +212,10 @@ class AllThreadsListTests(ThreadsListTestCase):
             self.access_all_categories()
 
             response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all'))
-            self.assertApiResultsAreEmpty(response)
+            self.assertEqual(response.status_code, 200)
+
+            response_json = response.json()
+            self.assertEqual(len(response_json['results']), 0)
 
         # empty lists render for anonymous user?
         self.logout_user()
@@ -237,7 +239,10 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.access_all_categories()
 
         response = self.client.get('%s?list=all' % self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_authenticated_only_views(self):
         """authenticated only views return 403 for guests"""
@@ -374,17 +379,32 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.assertTrue(positions['s'] < positions['l'])
         self.assertTrue(positions['s'] > positions['g'])
 
-        # locally pinned last
+        # pinned last
         self.assertTrue(positions['l'] > positions['g'])
         self.assertTrue(positions['l'] > positions['s'])
 
         # API behaviour is identic
         response = self.client.get('/api/threads/')
-        self.assertApiResultsEqual(response, [
-            globally,  # global announcement before others
-            standard,  # standard in the middle
-            locally,  # locally pinned last
-        ])
+        self.assertEqual(response.status_code, 200)
+
+        content = smart_str(response.content)
+        positions = {
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
+        }
+
+        # global announcement before others
+        self.assertTrue(positions['g'] < positions['l'])
+        self.assertTrue(positions['g'] < positions['s'])
+
+        # standard in the middle
+        self.assertTrue(positions['s'] < positions['l'])
+        self.assertTrue(positions['s'] > positions['g'])
+
+        # pinned last
+        self.assertTrue(positions['l'] > positions['g'])
+        self.assertTrue(positions['l'] > positions['s'])
 
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
@@ -532,11 +552,25 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         # API behaviour is identic
         response = self.client.get('/api/threads/?category=%s' % self.first_category.pk)
         self.assertEqual(response.status_code, 200)
-        self.assertApiResultsEqual(response, [
-            globally,  # global announcement before others
-            locally,  # locally pinned in the middle
-            standard,  # standard last
-        ])
+
+        content = smart_str(response.content)
+        positions = {
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
+        }
+
+        # global announcement before others
+        self.assertTrue(positions['g'] < positions['l'])
+        self.assertTrue(positions['g'] < positions['s'])
+
+        # pinned in the middle
+        self.assertTrue(positions['l'] < positions['s'])
+        self.assertTrue(positions['l'] > positions['g'])
+
+        # standard last
+        self.assertTrue(positions['s'] > positions['g'])
+        self.assertTrue(positions['s'] > positions['g'])
 
 
 class ThreadsVisibilityTests(ThreadsListTestCase):
@@ -547,6 +581,8 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         )
 
         response = self.client.get('/')
+        self.assertEqual(response.status_code, 200)
+
         self.assertContainsThread(response, test_thread)
 
         self.assertContains(response, 'subcategory-%s' % self.category_a.css_class)
@@ -558,9 +594,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # api displays same data
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 3)
         self.assertIn(self.category_a.pk, response_json['subcategories'])
 
@@ -578,9 +615,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # api displays same data
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk))
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 2)
         self.assertEqual(response_json['subcategories'][0], self.category_c.pk)
 
@@ -621,7 +659,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         )
 
         response = self.client.get(self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_see_own_unapproved_thread(self):
         """list renders unapproved thread that belongs to viewer"""
@@ -638,7 +679,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_cant_see_unapproved_thread(self):
         """list hides unapproved thread that belongs to other user"""
@@ -654,7 +698,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_cant_see_hidden_thread(self):
         """list hides hidden thread that belongs to other user"""
@@ -670,7 +717,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_cant_see_own_hidden_thread(self):
         """list hides hidden thread that belongs to viewer"""
@@ -687,7 +737,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_can_see_own_hidden_thread(self):
         """list shows hidden thread that belongs to viewer due to permission"""
@@ -707,7 +760,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get(self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_can_see_hidden_thread(self):
         """list shows hidden thread that belongs to other user due to permission"""
@@ -726,7 +782,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get(self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_can_see_unapproved_thread(self):
         """list shows hidden thread that belongs to other user due to permission"""
@@ -745,7 +804,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_approve_content': 1})
 
         response = self.client.get(self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
 class MyThreadsListTests(ThreadsListTestCase):
@@ -766,11 +828,16 @@ class MyThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertApiResultsAreEmpty(response)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_test_thread(self):
         """list renders only threads posted by user"""
@@ -798,11 +865,19 @@ class MyThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
 class NewThreadsListTests(ThreadsListTestCase):
@@ -823,11 +898,16 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertApiResultsAreEmpty(response)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_new_thread(self):
         """list renders new thread"""
@@ -848,11 +928,19 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_renders_thread_bumped_after_user_cutoff(self):
         """list renders new thread bumped after user cutoff"""
@@ -884,11 +972,19 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread started before global cutoff"""
@@ -915,11 +1011,17 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_cutoff_thread(self):
         """list hides thread started before users cutoff"""
@@ -946,11 +1048,17 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_read_thread(self):
         """list hides thread already read by user"""
@@ -976,11 +1084,17 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
 
 class UnreadThreadsListTests(ThreadsListTestCase):
@@ -1001,13 +1115,19 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_unread_thread(self):
         """list renders thread with unread posts"""
@@ -1035,13 +1155,21 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_hides_never_read_thread(self):
         """list hides never read thread"""
@@ -1065,13 +1193,19 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_read_thread(self):
         """list hides read thread"""
@@ -1097,13 +1231,19 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread replied before global cutoff"""
@@ -1134,13 +1274,19 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_cutoff_thread(self):
         """list hides thread replied before user cutoff"""
@@ -1174,13 +1320,19 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertNotInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
 
 
 class SubscribedThreadsListTests(ThreadsListTestCase):
@@ -1208,13 +1360,21 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertInApiResults(response, test_thread)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 1)
+        self.assertContains(response, test_thread.get_absolute_url())
 
     def test_list_hides_unsubscribed_thread(self):
         """list shows subscribed thread"""
@@ -1235,13 +1395,22 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotContainsThread(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertApiResultsAreEmpty(response)
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotContainsThread(response, test_thread)
+
 
 class UnapprovedListTests(ThreadsListTestCase):
     def test_list_errors_without_permission(self):
@@ -1316,7 +1485,9 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
 
         response = self.client.get('%s?list=unapproved' % self.api_link)
-        self.assertInApiResults(response, visible_thread)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
     def test_list_shows_owned_threads_for_unapproving_user(self):
         """list shows owned threads with unapproved posts for user without perm"""
@@ -1352,7 +1523,9 @@ class UnapprovedListTests(ThreadsListTestCase):
             'can_see_unapproved_content_lists': True,
         })
         response = self.client.get('%s?list=unapproved' % self.api_link)
-        self.assertInApiResults(response, visible_thread)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
 
 class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase):

+ 18 - 17
misago/threads/tests/test_threadview.py

@@ -2,10 +2,11 @@
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.conf import settings
-from misago.threads import moderation, testutils
+from misago.threads import testutils
 from misago.threads.checksums import update_post_checksum
 from misago.threads.events import record_event
-from misago.threads import moderation
+from misago.threads.moderation import threads as threads_moderation
+from misago.threads.moderation import hide_post
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
@@ -155,7 +156,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
     def test_hidden_post_visibility(self):
         """hidden post renders correctly"""
         post = testutils.reply_thread(self.thread, message="Hello, I'm hidden post!")
-        moderation.hide_post(self.user, post)
+        hide_post(self.user, post)
 
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -222,14 +223,14 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
     def test_thread_events_render(self):
         """different thread events render"""
         TEST_ACTIONS = [
-            (moderation.pin_thread_globally, "Thread has been pinned globally."),
-            (moderation.pin_thread_locally, "Thread has been pinned locally."),
-            (moderation.unpin_thread, "Thread has been unpinned."),
-            (moderation.approve_thread, "Thread has been approved."),
-            (moderation.close_thread, "Thread has been closed."),
-            (moderation.open_thread, "Thread has been opened."),
-            (moderation.hide_thread, "Thread has been made hidden."),
-            (moderation.unhide_thread, "Thread has been revealed."),
+            (threads_moderation.pin_thread_globally, "Thread has been pinned globally."),
+            (threads_moderation.pin_thread_locally, "Thread has been pinned locally."),
+            (threads_moderation.unpin_thread, "Thread has been unpinned."),
+            (threads_moderation.approve_thread, "Thread has been approved."),
+            (threads_moderation.close_thread, "Thread has been closed."),
+            (threads_moderation.open_thread, "Thread has been opened."),
+            (threads_moderation.hide_thread, "Thread has been made hidden."),
+            (threads_moderation.unhide_thread, "Thread has been revealed."),
         ]
 
         self.thread.is_unapproved = True
@@ -249,7 +250,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
             self.assertContains(response, message)
 
             # hidden events don't render without permission
-            moderation.hide_post(self.user, event)
+            hide_post(self.user, event)
             self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1})
 
             response = self.client.get(self.thread.get_absolute_url())
@@ -257,7 +258,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
             self.assertNotContains(response, message)
 
             # hidden event renders with permission
-            moderation.hide_post(self.user, event)
+            hide_post(self.user, event)
             self.override_acl({
                 'can_approve_content': 1,
                 'can_hide_threads': 1,
@@ -346,7 +347,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
 
     def test_changed_thread_title_event_renders(self):
         """changed thread title event renders"""
-        moderation.change_thread_title(
+        threads_moderation.change_thread_title(
             MockRequest(self.user), self.thread, "Lorem renamed ipsum!"
         )
 
@@ -364,7 +365,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
         self.thread.category = self.thread.category.parent
         self.thread.save()
 
-        moderation.move_thread(MockRequest(self.user), self.thread, self.category)
+        threads_moderation.move_thread(MockRequest(self.user), self.thread, self.category)
 
         event = self.thread.post_set.filter(is_event=True)[0]
         self.assertEqual(event.event_type, 'moved')
@@ -377,7 +378,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
     def test_thread_merged_event_renders(self):
         """merged thread event renders"""
         other_thread = testutils.post_thread(category=self.category)
-        moderation.merge_thread(MockRequest(self.user), self.thread, other_thread)
+        threads_moderation.merge_thread(MockRequest(self.user), self.thread, other_thread)
 
         event = self.thread.post_set.filter(is_event=True)[0]
         self.assertEqual(event.event_type, 'merged')
@@ -509,7 +510,7 @@ class ThreadAnonViewTests(ThreadViewTestCase):
         event = record_event(MockRequest(self.user), self.thread, 'closed')
 
         hidden_event = record_event(MockRequest(self.user), self.thread, 'opened')
-        moderation.hide_post(self.user, hidden_event)
+        hide_post(self.user, hidden_event)
 
         unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True)
         post = testutils.reply_thread(self.thread)

+ 44 - 0
misago/threads/tests/test_updatepostschecksums.py

@@ -0,0 +1,44 @@
+from django.core.management import call_command
+from django.test import TestCase
+from django.utils.six import StringIO
+
+from misago.categories.models import Category
+from misago.threads import testutils
+from misago.threads.management.commands import updatepostschecksums
+from misago.threads.models import Post
+
+
+class UpdatePostsChecksumsTests(TestCase):
+    def test_no_posts_to_update(self):
+        """command works when there are no posts"""
+        command = updatepostschecksums.Command()
+
+        out = StringIO()
+        call_command(command, stdout=out)
+        command_output = out.getvalue().strip()
+
+        self.assertEqual(command_output, "No posts were found")
+
+    def test_posts_update(self):
+        """command updates posts checksums"""
+        category = Category.objects.all_categories()[:1][0]
+
+        threads = [testutils.post_thread(category) for _ in range(5)]
+        for _, thread in enumerate(threads):
+            [testutils.reply_thread(thread) for _ in range(3)]
+            thread.save()
+
+        Post.objects.update(parsed='Hello world!')
+        for post in Post.objects.all():
+            self.assertFalse(post.is_valid)
+
+        command = updatepostschecksums.Command()
+
+        out = StringIO()
+        call_command(command, stdout=out)
+
+        command_output = out.getvalue().splitlines()[-1].strip()
+        self.assertEqual(command_output, "Updated 20 posts checksums")
+
+        for post in Post.objects.all():
+            self.assertTrue(post.is_valid)

+ 1 - 10
misago/threads/testutils.py

@@ -80,8 +80,7 @@ def reply_thread(
         is_protected=False,
         has_reports=False,
         has_open_reports=False,
-        posted_on=None,
-        poster_ip='127.0.0.1'
+        posted_on=None
 ):
     posted_on = posted_on or thread.last_post_on + timedelta(minutes=5)
 
@@ -91,7 +90,6 @@ def reply_thread(
         'original': message,
         'parsed': message,
         'checksum': 'nope',
-        'poster_ip': poster_ip,
         'posted_on': posted_on,
         'updated_on': posted_on,
         'is_event': is_event,
@@ -130,7 +128,6 @@ def post_poll(thread, poster):
         poster=poster,
         poster_name=poster.username,
         poster_slug=poster.slug,
-        poster_ip='127.0.0.1',
         question="Lorem ipsum dolor met?",
         choices=[
             {
@@ -170,7 +167,6 @@ def post_poll(thread, poster):
         voter=user,
         voter_name=user.username,
         voter_slug=user.slug,
-        voter_ip='127.0.0.1',
         choice_hash='aaaaaaaaaaaa',
     )
 
@@ -181,7 +177,6 @@ def post_poll(thread, poster):
         voter=poster,
         voter_name=poster.username,
         voter_slug=poster.slug,
-        voter_ip='127.0.0.1',
         choice_hash='gggggggggggg',
     )
     poll.pollvote_set.create(
@@ -190,7 +185,6 @@ def post_poll(thread, poster):
         voter=poster,
         voter_name=poster.username,
         voter_slug=poster.slug,
-        voter_ip='127.0.0.1',
         choice_hash='dddddddddddd',
     )
 
@@ -200,7 +194,6 @@ def post_poll(thread, poster):
         thread=thread,
         voter_name='deleted',
         voter_slug='deleted',
-        voter_ip='127.0.0.1',
         choice_hash='gggggggggggg',
     )
 
@@ -218,7 +211,6 @@ def like_post(post, liker=None, username=None):
             liker=liker,
             liker_name=liker.username,
             liker_slug=liker.slug,
-            liker_ip='127.0.0.1',
         )
 
         post.last_likes = [{
@@ -231,7 +223,6 @@ def like_post(post, liker=None, username=None):
             thread=post.thread,
             liker_name=username,
             liker_slug=slugify(username),
-            liker_ip='127.0.0.1',
         )
 
         post.last_likes = [{

+ 0 - 1
misago/threads/threadtypes/__init__.py

@@ -1,7 +1,6 @@
 from .treesmap import trees_map
 
 
-# fixme: a lot of those getters may be superficial
 class ThreadType(object):
     """Abstract class for thread type strategy"""
     root_name = 'undefined'

+ 2 - 2
misago/threads/threadtypes/privatethread.py

@@ -1,13 +1,13 @@
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 
-from misago.categories.models import PRIVATE_THREADS_ROOT
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 
 from . import ThreadType
 
 
 class PrivateThread(ThreadType):
-    root_name = PRIVATE_THREADS_ROOT
+    root_name = PRIVATE_THREADS_ROOT_NAME
 
     def get_category_name(self, category):
         return _('Private threads')

+ 2 - 2
misago/threads/threadtypes/thread.py

@@ -1,13 +1,13 @@
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 
-from misago.categories.models import THREADS_ROOT
+from misago.categories import THREADS_ROOT_NAME
 
 from . import ThreadType
 
 
 class Thread(ThreadType):
-    root_name = THREADS_ROOT
+    root_name = THREADS_ROOT_NAME
 
     def get_category_name(self, category):
         if category.level:

+ 1 - 1
misago/threads/urls/api.py

@@ -1,4 +1,4 @@
-from misago.api.router import MisagoApiRouter
+from misago.core.apirouter import MisagoApiRouter
 from misago.threads.api.attachments import AttachmentViewSet
 from misago.threads.api.threadpoll import ThreadPollViewSet
 from misago.threads.api.threadposts import PrivateThreadPostsViewSet, ThreadPostsViewSet

+ 3 - 2
misago/threads/validators.py

@@ -3,7 +3,8 @@ from django.utils.module_loading import import_string
 from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
-from misago.categories.models import THREADS_ROOT, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.categories.permissions import can_browse_category, can_see_category
 from misago.conf import settings
 from misago.core.validators import validate_sluggable
@@ -13,7 +14,7 @@ from .threadtypes import trees_map
 
 def validate_category(user, category_id, allow_root=False):
     try:
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT)
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
         category = Category.objects.get(
             tree_id=threads_tree_id,
             id=category_id,

+ 2 - 2
misago/threads/viewmodels/category.py

@@ -41,7 +41,7 @@ class ViewModel(BaseViewModel):
         return categories[0]
 
     def get_frontend_context(self):
-        return {'categories': BasicCategorySerializer(self._categories, many=True).data}
+        return {'CATEGORIES': BasicCategorySerializer(self._categories, many=True).data}
 
     def get_template_context(self):
         return {'category': self._model, 'subcategories': self._children}
@@ -88,5 +88,5 @@ class PrivateThreadsCategory(ViewModel):
 
 BasicCategorySerializer = CategorySerializer.subset_fields(
     'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'level', 'lft', 'rght', 'is_read'
+    'level', 'lft', 'rght', 'is_read', 'url'
 )

+ 4 - 3
misago/threads/viewmodels/thread.py

@@ -2,7 +2,8 @@ from django.shortcuts import get_object_or_404
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.categories.models import PRIVATE_THREADS_ROOT, THREADS_ROOT, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.core.shortcuts import validate_slug
 from misago.core.viewmodel import ViewModel as BaseViewModel
 from misago.readtracker.threadstracker import make_read_aware
@@ -105,7 +106,7 @@ class ForumThread(ViewModel):
         thread = get_object_or_404(
             Thread.objects.select_related(*BASE_RELATIONS),
             pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT),
+            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME),
         )
 
         allow_see_thread(request.user, thread)
@@ -127,7 +128,7 @@ class PrivateThread(ViewModel):
         thread = get_object_or_404(
             Thread.objects.select_related(*BASE_RELATIONS),
             pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT),
+            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME),
         )
 
         make_participants_aware(request.user, thread)

+ 2 - 2
misago/threads/viewmodels/threads.py

@@ -123,13 +123,13 @@ class ViewModel(object):
 
     def get_frontend_context(self):
         context = {
-            'threads': {
+            'THREADS': {
                 'results': ThreadsListSerializer(self.threads, many=True).data,
                 'subcategories': [c.pk for c in self.category.children],
             },
         }
 
-        context['threads'].update(self.paginator)
+        context['THREADS'].update(self.paginator)
         return context
 
     def get_template_context(self):

+ 1 - 1
misago/threads/views/list.py

@@ -21,7 +21,7 @@ class ThreadsList(View):
         threads = self.get_threads(request, category, list_type, page)
 
         frontend_context = self.get_frontend_context(request, category, threads)
-        request.frontend_context['store'].update(frontend_context)
+        request.frontend_context.update(frontend_context)
 
         template_context = self.get_template_context(request, category, threads)
         return render(request, self.template_name, template_context)

+ 9 - 3
misago/threads/views/thread.py

@@ -1,4 +1,5 @@
 from django.shortcuts import render
+from django.urls import reverse
 from django.views import View
 
 from misago.threads.viewmodels import ForumThread, PrivateThread, ThreadPosts
@@ -15,7 +16,7 @@ class ThreadBase(View):
         posts = self.get_posts(request, thread, page)
 
         frontend_context = self.get_frontend_context(request, thread, posts)
-        request.frontend_context['store'].update(frontend_context)
+        request.frontend_context.update(frontend_context)
 
         template_context = self.get_template_context(request, thread, posts)
         return render(request, self.template_name, template_context)
@@ -41,8 +42,8 @@ class ThreadBase(View):
         context = self.get_default_frontend_context()
 
         context.update({
-            'thread': thread.get_frontend_context(),
-            'posts': posts.get_frontend_context(),
+            'THREAD': thread.get_frontend_context(),
+            'POSTS': posts.get_frontend_context(),
         })
 
         return context
@@ -64,6 +65,11 @@ class ThreadView(ThreadBase):
     thread = ForumThread
     template_name = 'misago/thread/thread.html'
 
+    def get_default_frontend_context(self):
+        return {
+            'THREADS_API': reverse('misago:api:thread-list'),
+        }
+
 
 class PrivateThreadView(ThreadBase):
     thread = PrivateThread

+ 1 - 0
misago/urls.py

@@ -32,6 +32,7 @@ urlpatterns = [
 # Register API
 apipatterns = [
     url(r'^', include('misago.categories.urls.api')),
+    url(r'^', include('misago.legal.urls.api')),
     url(r'^', include('misago.markup.urls')),
     url(r'^', include('misago.threads.urls.api')),
     url(r'^', include('misago.users.urls.api')),

+ 19 - 0
misago/users/admin.py

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
 
 from .djangoadmin import UserAdminModel
 from .views.admin.bans import BansList, DeleteBan, EditBan, NewBan
+from .views.admin.datadownloads import DataDownloadsList, RequestDataDownloads
 from .views.admin.ranks import (
     DefaultRank, DeleteRank, EditRank, MoveDownRank, MoveUpRank, NewRank, RanksList, RankUsers)
 from .views.admin.users import (
@@ -65,6 +66,15 @@ class MisagoAdminExtension(object):
             url(r'^delete/(?P<pk>\d+)/$', DeleteBan.as_view(), name='delete'),
         )
 
+        # Data Downloads
+        urlpatterns.namespace(r'^data-downloads/', 'data-downloads', 'users')
+        urlpatterns.patterns(
+            'users:data-downloads',
+            url(r'^$', DataDownloadsList.as_view(), name='index'),
+            url(r'^(?P<page>\d+)/$', DataDownloadsList.as_view(), name='index'),
+            url(r'^request/$', RequestDataDownloads.as_view(), name='request'),
+        )
+        
     def register_navigation_nodes(self, site):
         site.add_node(
             name=_("Users"),
@@ -100,3 +110,12 @@ class MisagoAdminExtension(object):
             namespace='misago:admin:users:bans',
             link='misago:admin:users:bans:index',
         )
+
+        site.add_node(
+            name=_("Data downloads"),
+            icon='fa fa-download',
+            parent='misago:admin:users',
+            after='misago:admin:users:bans:index',
+            namespace='misago:admin:users:data-downloads',
+            link='misago:admin:users:data-downloads:index',
+        )

+ 126 - 78
misago/users/api/auth.py

@@ -1,18 +1,21 @@
+from rest_framework import status
 from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 
 from django.contrib import auth
+from django.contrib.auth.password_validation import validate_password
+from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext as _
 from django.views.decorators.csrf import csrf_protect
-from django.shortcuts import get_object_or_404
 
 from misago.conf import settings
+from misago.core.decorators import require_dict_data
 from misago.core.mail import mail_user
-from misago.users.serializers import (
-    AnonymousUserSerializer, AuthenticatedUserSerializer, ChangeForgottenPasswordSerializer,
-    LoginSerializer, ResendActivationSerializer, SendPasswordFormSerializer)
+from misago.users.bans import get_user_ban
+from misago.users.forms.auth import AuthenticationForm, ResendActivationForm, ResetPasswordForm
+from misago.users.serializers import AnonymousUserSerializer, AuthenticatedUserSerializer
 from misago.users.tokens import (
-    make_activation_token, make_password_change_token)
+    is_password_change_token_valid, make_activation_token, make_password_change_token)
 
 from .rest_permissions import UnbannedAnonOnly, UnbannedOnly
 
@@ -30,20 +33,23 @@ def gateway(request):
 @api_view(['POST'])
 @permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
+@require_dict_data
 def login(request):
     """
     POST /auth/ with CSRF, username and password
     will attempt to authenticate new user
     """
-    serializer = LoginSerializer(request, data=request.data)
-    serializer.is_valid(raise_exception=True)
-
-    user = serializer.validated_data['user']
-    auth.login(request, user)
-
-    return Response(
-        AuthenticatedUserSerializer(user).data,
-    )
+    form = AuthenticationForm(request, data=request.data)
+    if form.is_valid():
+        auth.login(request, form.user_cache)
+        return Response(
+            AuthenticatedUserSerializer(form.user_cache).data,
+        )
+    else:
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 @api_view()
@@ -58,9 +64,9 @@ def session_user(request):
 
 
 @api_view(['GET'])
-def get_requirements(request):
-    """GET /auth/requirements/ will return password and username requirements"""
-    requirements = {
+def get_criteria(request):
+    """GET /auth/criteria/ will return password and username criteria for accounts"""
+    criteria = {
         'username': {
             'min_length': settings.username_length_min,
             'max_length': settings.username_length_max,
@@ -70,82 +76,90 @@ def get_requirements(request):
 
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
         validator_dict = {'name': validator['NAME'].split('.')[-1]}
+
         validator_dict.update(validator.get('OPTIONS', {}))
-        requirements['password'].append(validator_dict)
 
-    return Response(requirements)
+        criteria['password'].append(validator_dict)
+
+    return Response(criteria)
 
 
 @api_view(['POST'])
 @permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
+@require_dict_data
 def send_activation(request):
     """
     POST /auth/send-activation/ with CSRF token and email
     will mail account activation link to requester
     """
-    serializer = ResendActivationSerializer(data=request.data)
-    serializer.is_valid(raise_exception=True)
-    serializer.raise_if_banned()
-
-    user = serializer.validated_data['user']
-
-    mail_subject = _("Activate %(user)s account on %(forum_name)s forums") % {
-        'user': user.username,
-        'forum_name': settings.forum_name,
-    }
-
-    mail_user(
-        request,
-        user,
-        mail_subject,
-        'misago/emails/activation/by_user',
-        {
-            'activation_token': make_activation_token(user),
-        },
-    )
-
-    return Response({
-        'username': user.username,
-        'email': user.email,
-    })
+    form = ResendActivationForm(request.data)
+    if form.is_valid():
+        requesting_user = form.user_cache
+
+        mail_subject = _("Activate %(user)s account on %(forum_name)s forums") % {
+            'user': requesting_user.username,
+            'forum_name': settings.forum_name,
+        }
+
+        mail_user(
+            requesting_user,
+            mail_subject,
+            'misago/emails/activation/by_user',
+            context={
+                'activation_token': make_activation_token(requesting_user),
+            },
+        )
+
+        return Response({
+            'username': form.user_cache.username,
+            'email': form.user_cache.email,
+        })
+    else:
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 @api_view(['POST'])
 @permission_classes((UnbannedOnly, ))
 @csrf_protect
+@require_dict_data
 def send_password_form(request):
     """
     POST /auth/send-password-form/ with CSRF token and email
     will mail change password form link to requester
     """
-    serializer = SendPasswordFormSerializer(data=request.data)
-    serializer.is_valid(raise_exception=True)
-    serializer.raise_if_banned()
-
-    user = serializer.validated_data['user']
-
-    mail_subject = _("Change %(user)s password on %(forum_name)s forums") % {
-        'user': user.username,
-        'forum_name': settings.forum_name,
-    }
-
-    confirmation_token = make_password_change_token(user)
-
-    mail_user(
-        request,
-        user,
-        mail_subject,
-        'misago/emails/change_password_form_link',
-        {
-            'confirmation_token': confirmation_token,
-        },
-    )
-
-    return Response({
-        'username': user.username,
-        'email': user.email,
-    })
+    form = ResetPasswordForm(request.data)
+    if form.is_valid():
+        requesting_user = form.user_cache
+
+        mail_subject = _("Change %(user)s password on %(forum_name)s forums") % {
+            'user': requesting_user.username,
+            'forum_name': settings.forum_name,
+        }
+
+        confirmation_token = make_password_change_token(requesting_user)
+
+        mail_user(
+            requesting_user,
+            mail_subject,
+            'misago/emails/change_password_form_link',
+            context={
+                'confirmation_token': confirmation_token,
+            },
+        )
+
+        return Response({
+            'username': form.user_cache.username,
+            'email': form.user_cache.email,
+        })
+    else:
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 class PasswordChangeFailed(Exception):
@@ -155,15 +169,49 @@ class PasswordChangeFailed(Exception):
 @api_view(['POST'])
 @permission_classes((UnbannedOnly, ))
 @csrf_protect
-def change_forgotten_password(request, pk):
+@require_dict_data
+def change_forgotten_password(request, pk, token):
     """
-    POST /auth/change-password/user/ with CSRF and new password
+    POST /auth/change-password/user/token/ with CSRF and new password
     will change forgotten password
     """
-    user = get_object_or_404(UserModel, pk=pk, is_active=True)
-    serializer = ChangeForgottenPasswordSerializer(user, data=request.data)
-    serializer.is_valid(raise_exception=True)
-
-    serializer.save()
+    invalid_message = _("Form link is invalid. Please try again.")
+    expired_message = _("Your link has expired. Please request new one.")
+
+    try:
+        try:
+            user = UserModel.objects.get(pk=pk, is_active=True)
+        except UserModel.DoesNotExist:
+            raise PasswordChangeFailed(invalid_message)
+
+        if request.user.is_authenticated and request.user.id != user.id:
+            raise PasswordChangeFailed(invalid_message)
+        if not is_password_change_token_valid(user, token):
+            raise PasswordChangeFailed(invalid_message)
+
+        if user.requires_activation:
+            raise PasswordChangeFailed(expired_message)
+        if get_user_ban(user):
+            raise PasswordChangeFailed(expired_message)
+    except PasswordChangeFailed as e:
+        return Response(
+            {
+                'detail': e.args[0],
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
+
+    try:
+        new_password = request.data.get('password', '')
+        validate_password(new_password, user=user)
+        user.set_password(new_password)
+        user.save()
+    except ValidationError as e:
+        return Response(
+            {
+                'detail': e.messages[0],
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
     return Response({'username': user.username})

+ 24 - 20
misago/users/api/userendpoints/avatar.py

@@ -1,5 +1,6 @@
 import json
 
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.core.exceptions import ValidationError
@@ -17,16 +18,16 @@ from misago.users.serializers import ModerateAvatarSerializer
 def avatar_endpoint(request, pk=None):
     if request.user.is_avatar_locked:
         if request.user.avatar_lock_user_message:
-            extra = format_plaintext_for_html(request.user.avatar_lock_user_message)
+            reason = format_plaintext_for_html(request.user.avatar_lock_user_message)
         else:
-            extra = None
+            reason = None
 
         return Response(
             {
                 'detail': _("Your avatar is locked. You can't change it."),
-                'extra': extra,
+                'reason': reason,
             },
-            status=403,
+            status=status.HTTP_403_FORBIDDEN,
         )
 
     avatar_options = get_avatar_options(request.user)
@@ -109,7 +110,7 @@ def avatar_post(options, user, data):
                 {
                     'detail': _("This avatar type is not allowed."),
                 },
-                status=400,
+                status=status.HTTP_400_BAD_REQUEST,
             )
 
         rpc_handler = AVATAR_TYPES[data.get('avatar', 'nope')]
@@ -118,7 +119,7 @@ def avatar_post(options, user, data):
             {
                 'detail': _("Unknown avatar type."),
             },
-            status=400,
+            status=status.HTTP_400_BAD_REQUEST,
         )
 
     try:
@@ -128,7 +129,7 @@ def avatar_post(options, user, data):
             {
                 'detail': e.args[0],
             },
-            status=400,
+            status=status.HTTP_400_BAD_REQUEST,
         )
 
     user.save()
@@ -209,20 +210,23 @@ AVATAR_TYPES = {
 def moderate_avatar_endpoint(request, profile):
     if request.method == "POST":
         is_avatar_locked = profile.is_avatar_locked
-
         serializer = ModerateAvatarSerializer(profile, data=request.data)
-        serializer.is_valid(raise_exception=True)
-
-        if serializer.validated_data['is_avatar_locked'] and not is_avatar_locked:
-            avatars.dynamic.set_avatar(profile)
-        serializer.save()
-
-        return Response({
-            'avatars': profile.avatars,
-            'is_avatar_locked': int(profile.is_avatar_locked),
-            'avatar_lock_user_message': profile.avatar_lock_user_message,
-            'avatar_lock_staff_message': profile.avatar_lock_staff_message,
-        })
+        if serializer.is_valid():
+            if serializer.validated_data['is_avatar_locked'] and not is_avatar_locked:
+                avatars.dynamic.set_avatar(profile)
+            serializer.save()
+
+            return Response({
+                'avatars': profile.avatars,
+                'is_avatar_locked': int(profile.is_avatar_locked),
+                'avatar_lock_user_message': profile.avatar_lock_user_message,
+                'avatar_lock_staff_message': profile.avatar_lock_staff_message,
+            })
+        else:
+            return Response(
+                serializer.errors,
+                status=status.HTTP_400_BAD_REQUEST,
+            )
     else:
         return Response({
             'is_avatar_locked': int(profile.is_avatar_locked),

+ 14 - 11
misago/users/api/userendpoints/changeemail.py

@@ -1,3 +1,4 @@
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.utils.translation import ugettext as _
@@ -14,18 +15,20 @@ def change_email_endpoint(request, pk=None):
         context={'user': request.user},
     )
 
-    serializer.is_valid(raise_exception=True)
+    if serializer.is_valid():
+        token = store_new_credential(request, 'email', serializer.validated_data['new_email'])
 
-    token = store_new_credential(request, 'email', serializer.validated_data['new_email'])
+        mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
+        mail_subject = mail_subject % {'forum_name': settings.forum_name}
 
-    mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
-    mail_subject = mail_subject % {'forum_name': settings.forum_name}
+        # swap address with new one so email is sent to new address
+        request.user.email = serializer.validated_data['new_email']
 
-    # swap address with new one so email is sent to new address
-    request.user.email = serializer.validated_data['new_email']
+        mail_user(
+            request.user, mail_subject, 'misago/emails/change_email', context={'token': token}
+        )
 
-    mail_user(
-        request, request.user, mail_subject, 'misago/emails/change_email', {'token': token}
-    )
-
-    return Response(status=204)
+        message = _("E-mail change confirmation link was sent to new address.")
+        return Response({'detail': message})
+    else:
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

+ 16 - 11
misago/users/api/userendpoints/changepassword.py

@@ -1,3 +1,4 @@
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.utils.translation import ugettext as _
@@ -14,17 +15,21 @@ def change_password_endpoint(request, pk=None):
         context={'user': request.user},
     )
 
-    serializer.is_valid(raise_exception=True)
+    if serializer.is_valid():
+        token = store_new_credential(
+            request, 'password', serializer.validated_data['new_password']
+        )
 
-    token = store_new_credential(
-        request, 'password', serializer.validated_data['new_password']
-    )
-
-    mail_subject = _("Confirm password change on %(forum_name)s forums")
-    mail_subject = mail_subject % {'forum_name': settings.forum_name}
+        mail_subject = _("Confirm password change on %(forum_name)s forums")
+        mail_subject = mail_subject % {'forum_name': settings.forum_name}
 
-    mail_user(
-        request, request.user, mail_subject, 'misago/emails/change_password', {'token': token}
-    )
+        mail_user(
+            request.user, mail_subject, 'misago/emails/change_password',
+            context={'token': token}
+        )
 
-    return Response(status=204)
+        return Response({
+            'detail': _("Password change confirmation link was sent to your address.")
+        })
+    else:
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

+ 28 - 20
misago/users/api/userendpoints/create.py

@@ -1,3 +1,4 @@
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.contrib.auth import authenticate, get_user_model, login
@@ -7,10 +8,12 @@ from django.utils.translation import ugettext as _
 from django.views.decorators.csrf import csrf_protect
 
 from misago.conf import settings
-from misago.core.exceptions import Banned
-from misago.users.bans import get_ip_ban
-from misago.users.serializers import RegisterUserSerializer
-from misago.users.registration import get_registration_result_json, send_welcome_email
+from misago.legal.models import Agreement
+from misago.users import captcha
+from misago.users.forms.register import RegisterForm
+from misago.users.registration import (
+    get_registration_result_json, save_user_agreements, send_welcome_email
+)
 
 
 UserModel = get_user_model()
@@ -21,16 +24,20 @@ def create_endpoint(request):
     if settings.account_activation == 'closed':
         raise PermissionDenied(_("New users registrations are currently closed."))
 
-    ban = get_ip_ban(request.user_ip, registration_only=True)
-    if ban:
-        raise Banned(ban)
-
-    serializer = RegisterUserSerializer(
-        data=request.data,
-        context={'request': request},
+    form = RegisterForm(
+        request.data,
+        request=request,
+        agreements=Agreement.objects.get_agreements(),
     )
 
-    serializer.is_valid(raise_exception=True)
+    try:
+        if form.is_valid():
+            captcha.test_request(request)
+    except ValidationError as e:
+        form.add_error('captcha', e)
+
+    if not form.is_valid():
+        return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
 
     activation_kwargs = {}
     if settings.account_activation == 'user':
@@ -40,9 +47,10 @@ def create_endpoint(request):
 
     try:
         new_user = UserModel.objects.create_user(
-            serializer.validated_data['username'],
-            serializer.validated_data['email'],
-            serializer.validated_data['password'],
+            form.cleaned_data['username'],
+            form.cleaned_data['email'],
+            form.cleaned_data['password'],
+            create_audit_trail=True,
             joined_from_ip=request.user_ip,
             set_default_avatar=True,
             **activation_kwargs
@@ -50,17 +58,17 @@ def create_endpoint(request):
     except IntegrityError:
         return Response(
             {
-                'detail': _("Please try resubmitting the form."),
+                '__all__': _("Please try resubmitting the form.")
             },
-            status=400,
+            status=status.HTTP_400_BAD_REQUEST,
         )
 
+    save_user_agreements(new_user, form)
     send_welcome_email(request, new_user)
 
-    if not new_user.requires_activation == 'none':
+    if new_user.requires_activation == UserModel.ACTIVATION_NONE:
         authenticated_user = authenticate(
-            username=new_user.email,
-            password=serializer.validated_data['password'],
+            username=new_user.email, password=form.cleaned_data['password']
         )
         login(request, authenticated_user)
 

+ 17 - 14
misago/users/api/userendpoints/signature.py

@@ -1,3 +1,4 @@
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.core.exceptions import PermissionDenied
@@ -17,17 +18,15 @@ def signature_endpoint(request):
 
     if user.is_signature_locked:
         if user.signature_lock_user_message:
-            extra = format_plaintext_for_html(user.signature_lock_user_message)
+            reason = format_plaintext_for_html(user.signature_lock_user_message)
         else:
-            extra = None
+            reason = None
 
-        return Response(
-            {
-                'detail': _("Your signature is locked. You can't change it."),
-                'extra': extra
-            },
-            status=403,
-        )
+        return Response({
+            'detail': _("Your signature is locked. You can't change it."),
+            'reason': reason
+        },
+                        status=status.HTTP_403_FORBIDDEN)
 
     if request.method == 'POST':
         return edit_signature(request, user)
@@ -55,8 +54,12 @@ def get_signature_options(user):
 
 def edit_signature(request, user):
     serializer = EditSignatureSerializer(user, data=request.data)
-    serializer.is_valid(raise_exception=True)
-
-    set_user_signature(request, user, serializer.validated_data['signature'])
-    user.save(update_fields=['signature', 'signature_parsed', 'signature_checksum'])
-    return get_signature_options(user)
+    if serializer.is_valid():
+        set_user_signature(request, user, serializer.validated_data['signature'])
+        user.save(update_fields=['signature', 'signature_parsed', 'signature_checksum'])
+        return get_signature_options(user)
+    else:
+        return Response({
+            'detail': serializer.errors['non_field_errors'][0]
+        },
+                        status=status.HTTP_400_BAD_REQUEST)

+ 58 - 51
misago/users/api/userendpoints/username.py

@@ -1,10 +1,11 @@
+from rest_framework import status
 from rest_framework.response import Response
 
 from django.db import IntegrityError
 from django.utils.translation import ugettext as _
 
 from misago.conf import settings
-from misago.users.namechanges import get_available_namechanges_data
+from misago.users.namechanges import UsernameChanges
 from misago.users.serializers import ChangeUsernameSerializer
 
 
@@ -12,72 +13,78 @@ def username_endpoint(request):
     if request.method == 'POST':
         return change_username(request)
     else:
-        form_options = get_available_namechanges_data(request.user)
-        form_options.update({
-            'length_min': settings.username_length_min,
-            'length_max': settings.username_length_max,
-        })
-        
-        return Response(form_options)
+        return options_response(get_username_options(request.user))
 
 
-def change_username(request):
-    available_namechanges = get_available_namechanges_data(request.user)
-    if not available_namechanges['changes_left']:
-        return Response(
-            {
-                'username': [_("You can't change your username at this time.")],
-                'next_change_on': available_namechanges['next_change_on'],
-            },
-            status=400,
-        )
+def get_username_options(user):
+    options = UsernameChanges(user)
+    return {
+        'changes_left': options.left,
+        'next_on': options.next_on,
+        'length_min': settings.username_length_min,
+        'length_max': settings.username_length_max,
+    }
 
-    serializer = ChangeUsernameSerializer(
-        data=request.data,
-        context={'user': request.user},
-    )
 
-    serializer.is_valid(raise_exception=True)
+def options_response(options):
+    if options['next_on']:
+        options['next_on'] = options['next_on'].isoformat()
+    return Response(options)
 
-    try:
-        serializer.change_username(changed_by=request.user)
 
-        response_data = get_available_namechanges_data(request.user)
-        response_data.update({
-            'username': request.user.username,
-            'slug': request.user.slug,
-        })
+def change_username(request):
+    options = get_username_options(request.user)
+    if not options['changes_left']:
+        return Response({
+            'detail': _("You can't change your username now."),
+            'options': options
+        },
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    serializer = ChangeUsernameSerializer(data=request.data, context={'user': request.user})
 
-        return Response(response_data)
-    except IntegrityError:
-        return Response(
-            {
-                'username': [_("Please try again.")],
+    if serializer.is_valid():
+        try:
+            serializer.change_username(changed_by=request.user)
+            return Response({
+                'username': request.user.username,
+                'slug': request.user.slug,
+                'options': get_username_options(request.user)
+            })
+        except IntegrityError:
+            return Response({
+                'detail': _("Error changing username. Please try again."),
             },
-            status=400,
-        )
+                            status=status.HTTP_400_BAD_REQUEST)
+    else:
+        return Response({
+            'detail': serializer.errors['non_field_errors'][0]
+        },
+                        status=status.HTTP_400_BAD_REQUEST)
 
 
 def moderate_username_endpoint(request, profile):
     if request.method == 'POST':
         serializer = ChangeUsernameSerializer(data=request.data, context={'user': profile})
-        serializer.is_valid(raise_exception=True)
 
-        try:
-            serializer.change_username(changed_by=request.user)
-            return Response({
-                'username': profile.username,
-                'slug': profile.slug,
-            })
-        except IntegrityError:
-            return Response(
-                {
-                    'username': [_("Please try again.")],
+        if serializer.is_valid():
+            try:
+                serializer.change_username(changed_by=request.user)
+                return Response({
+                    'username': profile.username,
+                    'slug': profile.slug,
+                })
+            except IntegrityError:
+                return Response({
+                    'detail': _("Error changing username. Please try again."),
                 },
-                status=400,
-            )
+                                status=status.HTTP_400_BAD_REQUEST)
+        else:
+            return Response({
+                'detail': serializer.errors['non_field_errors'][0]
+            },
+                            status=status.HTTP_400_BAD_REQUEST)
     else:
-        # return form data
         return Response({
             'length_min': settings.username_length_min,
             'length_max': settings.username_length_max,

+ 40 - 11
misago/users/api/users.py

@@ -1,4 +1,4 @@
-from rest_framework import viewsets
+from rest_framework import status, viewsets
 from rest_framework.decorators import detail_route
 from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
 from rest_framework.response import Response
@@ -12,18 +12,21 @@ from django.shortcuts import get_object_or_404
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
-from misago.api.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.categories.models import Category
+from misago.conf import settings
+from misago.core.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.core.shortcuts import get_int_or_404
 from misago.threads.moderation import hide_post, hide_thread
 from misago.users.bans import get_user_ban
+from misago.users.datadownloads import request_user_data_download, user_has_data_download_request
 from misago.users.online.utils import get_user_status
 from misago.users.permissions import (
     allow_browse_users_list, allow_delete_user, allow_edit_profile_details, allow_follow_user,
     allow_moderate_avatar, allow_rename_user, allow_see_ban_details)
 from misago.users.profilefields import profilefields, serialize_profilefields_data
 from misago.users.serializers import (
-    BanDetailsSerializer, DeleteOwnAccountSerializer, ForumOptionsSerializer, UserSerializer)
+    BanDetailsSerializer, DataDownloadSerializer, DeleteOwnAccountSerializer, ForumOptionsSerializer,
+    UserSerializer)
 from misago.users.viewmodels import Followers, Follows, UserPosts, UserThreads
 
 from .rest_permissions import BasePermission, UnbannedAnonOnly
@@ -107,10 +110,11 @@ class UserViewSet(viewsets.GenericViewSet):
         allow_self_only(request.user, pk, _("You can't change other users options."))
 
         serializer = ForumOptionsSerializer(request.user, data=request.data)
-        serializer.is_valid(raise_exception=True)
-
-        serializer.save()
-        return Response(status=204)
+        if serializer.is_valid():
+            serializer.save()
+            return Response({'detail': _("Your forum options have been changed.")})
+        else:
+            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
     @detail_route(methods=['get', 'post'])
     def username(self, request, pk=None):
@@ -188,10 +192,7 @@ class UserViewSet(viewsets.GenericViewSet):
             profile.save(update_fields=['followers'])
             request.user.save(update_fields=['following'])
 
-            return Response({
-                'is_followed': followed,
-                'followers': profile_followers,
-            })
+            return Response({'is_followed': followed, 'followers': profile_followers})
 
     @detail_route()
     def ban(self, request, pk=None):
@@ -218,6 +219,22 @@ class UserViewSet(viewsets.GenericViewSet):
 
         return moderate_username_endpoint(request, profile)
 
+    @detail_route(methods=['post'])
+    def request_data_download(self, request, pk=None):
+        get_int_or_404(pk)
+        allow_self_only(request.user, pk, _("You can't request data downloads for other users."))
+
+        if not settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA:
+            raise PermissionDenied(_("You can't download your data."))
+
+        if user_has_data_download_request(request.user):
+            raise PermissionDenied(
+                _("You can't have more than one data download request at single time."))
+            
+        request_user_data_download(request.user)
+
+        return Response({'detail': 'ok'})
+
     @detail_route(methods=['get', 'post'])
     def delete(self, request, pk=None):
         profile = self.get_user(request, pk)
@@ -256,6 +273,15 @@ class UserViewSet(viewsets.GenericViewSet):
         return Response({})
 
     @detail_route(methods=['get'])
+    def data_downloads(self, request, pk=None):
+        get_int_or_404(pk)
+        allow_self_only(request.user, pk, _("You can't see other users data downloads."))
+
+        queryset = request.user.datadownload_set.all()[:5]
+        serializer = DataDownloadSerializer(queryset, many=True)
+        return Response(serializer.data)
+
+    @detail_route(methods=['get'])
     def followers(self, request, pk=None):
         profile = self.get_user(request, pk)
 
@@ -324,8 +350,11 @@ UserProfileSerializer = UserSerializer.subset_fields(
     'following',
     'threads',
     'posts',
+    'acl',
     'is_followed',
     'is_blocked',
     'real_name',
     'status',
+    'api',
+    'url',
 )

+ 8 - 0
misago/users/apps.py

@@ -44,6 +44,14 @@ class MisagoUsersConfig(AppConfig):
             icon='vpn_key',
         )
 
+        if settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA:
+            usercp.add_section(
+                link='misago:usercp-download-data',
+                name=_("Download data"),
+                component='download-data',
+                icon='save_alt',
+            )
+
         if settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT:
             usercp.add_section(
                 link='misago:usercp-delete-account',

+ 19 - 0
misago/users/audittrail.py

@@ -0,0 +1,19 @@
+from django.db import models
+
+
+def create_audit_trail(request, obj):
+    return create_user_audit_trail(request.user, request.user_ip, obj)
+
+
+def create_user_audit_trail(user, ip_address, obj):
+    if not isinstance(obj, models.Model):
+        raise ValueError("obj must be a valid Django model instance")
+
+    if user.is_anonymous:
+        return None
+
+    return user.audittrail_set.create(
+        user=user,
+        ip_address=ip_address,
+        content_object=obj,
+    )

+ 0 - 73
misago/users/authmixin.py

@@ -1,73 +0,0 @@
-from django.contrib.auth import authenticate, get_user_model
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy, ugettext as _
-
-from misago.core.exceptions import Banned
-from misago.users.bans import get_user_ban
-
-
-UserModel = get_user_model()
-
-
-class AuthMixin(object):
-    """
-    Mixin with utils for Auth forms and serializers
-    """
-    auth_messages = {
-        'empty_data': ugettext_lazy("Fill out both fields."),
-        'invalid_login': ugettext_lazy("Login or password is incorrect."),
-        'inactive_user': ugettext_lazy("You have to activate your account before you will be able to sign in."),
-        'inactive_admin': ugettext_lazy(
-            "Your account has to be activated by Administrator before you will be able to sign in."
-        ),
-    }
-
-    def authenticate(self, username, password):
-        if username and password:
-            user = authenticate(username=username, password=password)
-
-            if user is None or not user.is_active:
-                self.raise_for_code('invalid_login')
-        else:
-            self.raise_for_code('empty_data')
-
-        return user
-
-    def get_user_by_email(self, email):
-        if not email:
-            return None
-
-        try:
-            user = UserModel.objects.get_by_email(email)
-            if not user.is_active:
-                raise UserModel.DoesNotExist()
-            return user
-        except UserModel.DoesNotExist:
-            raise ValidationError(_("No user with this e-mail exists."))
-
-    def confirm_login_allowed(self, user):
-        self.confirm_user_active(user)
-        self.confirm_user_not_banned(user)
-
-    def confirm_user_active(self, user):
-        if user.requires_activation_by_admin:
-            self.raise_for_code('inactive_admin')
-        if user.requires_activation_by_user:
-            self.raise_for_code('inactive_user')
-
-    def confirm_user_not_banned(self, user):
-        ban = self.get_user_ban(user)
-        if ban:
-            raise Banned(ban=ban)
-
-    def get_user_ban(self, user):
-        if user.is_staff:
-            return None
-        return get_user_ban(user)
-
-    def raise_if_banned(self):
-        user = self.validated_data.get('user')
-        self.confirm_user_not_banned(user)
-
-    def raise_for_code(self, code):
-        raise ValidationError(self.auth_messages[code], code=code)

+ 4 - 4
misago/users/avatars/gallery.py

@@ -65,8 +65,8 @@ def load_avatar_galleries():
     return galleries
 
 
-def set_avatar(user, image):
-    store.store_new_avatar(user, Image.open(image.path))
+def set_avatar(user, avatar):
+    store.store_new_avatar(user, Image.open(avatar.image))
 
 
 def set_random_avatar(user):
@@ -82,5 +82,5 @@ def set_random_avatar(user):
         else:
             avatars_list += gallery['images']
 
-    random_image_path = random.choice(avatars_list).path
-    store.store_new_avatar(user, Image.open(random_image_path))
+    random_avatar = random.choice(avatars_list)
+    store.store_new_avatar(user, Image.open(random_avatar.image))

+ 5 - 5
misago/users/bans.py

@@ -11,11 +11,11 @@ from django.utils.dateparse import parse_datetime
 
 from misago.core import cachebuster
 
-from .constants import BANS_CACHEBUSTER
 from .models import Ban, BanCache
 
 
 CACHE_SESSION_KEY = 'misago_ip_check'
+VERSION_KEY = 'misago_bans'
 
 
 def get_username_ban(username, registration_only=False):
@@ -63,7 +63,7 @@ def get_user_ban(user):
 
 def _set_user_ban_cache(user):
     ban_cache = user.ban_cache
-    ban_cache.bans_version = cachebuster.get_version(BANS_CACHEBUSTER)
+    ban_cache.bans_version = cachebuster.get_version(VERSION_KEY)
 
     try:
         user_ban = Ban.objects.get_ban(
@@ -103,7 +103,7 @@ def get_request_ip_ban(request):
     found_ban = get_ip_ban(request.user_ip)
 
     ban_cache = request.session[CACHE_SESSION_KEY] = {
-        'version': cachebuster.get_version(BANS_CACHEBUSTER),
+        'version': cachebuster.get_version(VERSION_KEY),
         'ip': request.user_ip,
     }
 
@@ -128,10 +128,10 @@ def _get_session_bancache(request):
         ban_cache = _hydrate_session_cache(ban_cache)
         if ban_cache['ip'] != request.user_ip:
             return None
-        if not cachebuster.is_valid(BANS_CACHEBUSTER, ban_cache['version']):
+        if not cachebuster.is_valid(VERSION_KEY, ban_cache['version']):
             return None
         if ban_cache.get('expires_on'):
-            if ban_cache['expires_on'] < timezone.now():
+            if ban_cache['expires_on'] < timezone.today():
                 return None
         return ban_cache
     except KeyError:

+ 20 - 9
misago/users/context_processors.py

@@ -1,8 +1,25 @@
+from django.urls import reverse
+
 from .pages import user_profile, usercp, users_list
 from .serializers import AnonymousUserSerializer, AuthenticatedUserSerializer
 
 
 def user_links(request):
+    if request.include_frontend_context:
+        request.frontend_context.update({
+            'REQUEST_ACTIVATION_URL': reverse('misago:request-activation'),
+            'FORGOTTEN_PASSWORD_URL': reverse('misago:forgotten-password'),
+            'BANNED_URL': reverse('misago:banned'),
+            'USERCP_URL': reverse('misago:options'),
+            'USERS_LIST_URL': reverse('misago:users'),
+            'AUTH_API': reverse('misago:api:auth'),
+            'AUTH_CRITERIA_API': reverse('misago:api:auth-criteria'),
+            'USERS_API': reverse('misago:api:user-list'),
+            'CAPTCHA_API': reverse('misago:api:captcha-question'),
+            'USERNAME_CHANGES_API': reverse('misago:api:usernamechange-list'),
+            'MENTION_API': reverse('misago:api:mention-suggestions'),
+        })
+
     return {
         'USERCP_URL': usercp.get_default_link(),
         'USERS_LIST_URL': users_list.get_default_link(),
@@ -14,19 +31,13 @@ def preload_user_json(request):
     if not request.include_frontend_context:
         return {}
 
-    request.frontend_context['auth'].update({
-        'id': request.user.id,
-        'isAnonymous': bool(request.user.is_anonymous),
+    request.frontend_context.update({
         'isAuthenticated': bool(request.user.is_authenticated),
     })
 
     if request.user.is_authenticated:
-        request.frontend_context['store'].update({
-            'auth': AuthenticatedUserSerializer(request.user).data,
-        })
+        request.frontend_context.update({'user': AuthenticatedUserSerializer(request.user).data})
     else:
-        request.frontend_context['store'].update({
-            'auth': AnonymousUserSerializer(request.user).data,
-        })
+        request.frontend_context.update({'user': AnonymousUserSerializer(request.user).data})
 
     return {}

+ 53 - 0
misago/users/datadownloads/__init__.py

@@ -0,0 +1,53 @@
+from datetime import timedelta
+
+from django.utils import timezone
+
+from misago.conf import settings
+from misago.users.models import DataDownload
+from misago.users.signals import archive_user_data
+
+from .dataarchive import DataArchive
+
+
+STATUS_REQUEST = (DataDownload.STATUS_PENDING, DataDownload.STATUS_PROCESSING)
+
+
+def user_has_data_download_request(user):
+    queryset = DataDownload.objects.filter(user=user, status__in=STATUS_REQUEST)
+    return queryset.exists()
+
+
+def request_user_data_download(user, requester=None):
+    requester = requester or user
+
+    return DataDownload.objects.create(
+        user=user,
+        requester=requester,
+        requester_name=requester.username,
+    )
+
+
+def prepare_user_data_download(download, logger=None):
+    working_dir = settings.MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR
+    user = download.user
+    with DataArchive(user, working_dir) as archive:
+        try:
+            archive_user_data.send(user, archive=archive)
+            download.status = DataDownload.STATUS_READY
+            download.expires_on = timezone.now() + timedelta(
+                hours=settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS)
+            download.file = archive.get_file()
+            download.save()
+            # todo: send an e-mail with download link
+            return True
+        except Exception as e:
+            if logger:
+                logger.exception(e)
+            return False
+
+
+def expire_user_data_download(download):
+    download.status = DataDownload.STATUS_EXPIRED
+    if download.file:
+        download.file.delete(save=False)
+    download.save()

+ 156 - 0
misago/users/datadownloads/dataarchive.py

@@ -0,0 +1,156 @@
+import io  # fixme: remove explicit io imports after going py3k-only
+import os
+import shutil
+
+from django.core.files import File
+from django.utils import timezone
+from django.utils.crypto import get_random_string
+from django.utils import six
+
+from misago.core.utils import slugify
+
+
+FILENAME_MAX_LEN = 50
+
+
+class DataArchive(object):
+    def __init__(self, user, working_dir_path):
+        self.user = user
+        self.working_dir_path = working_dir_path
+
+        self.tmp_dir_path = None
+        self.data_dir_path = None
+
+        self.file_path = None
+        self.file = None
+
+    def __enter__(self):
+        self.tmp_dir_path = self.create_tmp_dir()
+        self.data_dir_path = self.create_data_dir()
+
+        return self
+
+    def __exit__(self, *args):
+        self.delete_file()
+        self.delete_tmp_dir()
+
+    def create_tmp_dir(self):
+        tmp_dir_name = get_tmp_filename(self.user)
+        tmp_dir_path = os.path.join(self.working_dir_path, tmp_dir_name)
+
+        os.mkdir(tmp_dir_path)
+
+        return tmp_dir_path
+
+    def create_data_dir(self):
+        data_dir_name = get_tmp_filename(self.user)
+        data_dir_path = os.path.join(self.tmp_dir_path, data_dir_name)
+
+        os.mkdir(data_dir_path)
+
+        return data_dir_path
+
+    def delete_tmp_dir(self):
+        if self.tmp_dir_path:
+            shutil.rmtree(self.tmp_dir_path)
+            self.tmp_dir_path = None
+            self.data_dir_path = None
+            
+    def get_file(self):
+        file_name = get_tmp_filename(self.user)
+        file_path = os.path.join(self.working_dir_path, file_name)
+
+        self.file_path = shutil.make_archive(file_path, 'zip', self.tmp_dir_path)
+        self.file = open(self.file_path, 'rb')
+
+        return File(self.file)
+
+    def delete_file(self):
+        if self.file:
+            self.file.close()
+            self.file = None
+
+        if self.file_path:
+            os.remove(self.file_path)
+            self.file_path = None
+
+    def add_text(self, name, value, date=None, directory=None):
+        clean_filename = slugify(str(name))
+        file_dir_path = self.make_final_path(date=date, directory=directory)
+        file_path = os.path.join(file_dir_path, '{}.txt'.format(clean_filename))
+        with io.open(file_path, 'w', encoding='utf-8') as fp:
+            fp.write(six.text_type(value))
+            return file_path
+
+    def add_dict(self, name, value, date=None, directory=None):
+        text_lines = []
+        for key, value in value.items():
+            text_lines.append(u"{}: {}".format(key, value))
+        text = u'\n'.join(text_lines)
+        return self.add_text(name, text, date=date, directory=directory)
+
+    def add_model_file(self, model_file, prefix=None, date=None, directory=None):
+        if not model_file:
+            return None
+
+        target_dir_path = self.make_final_path(date=date, directory=directory)
+
+        filename = os.path.basename(model_file.name)
+        if prefix:
+            prefixed_filename = u"{}-{}".format(prefix, filename)
+            clean_filename = trim_long_filename(prefixed_filename)
+            target_path = os.path.join(target_dir_path, clean_filename)
+        else:
+            clean_filename = trim_long_filename(filename)
+            target_path = os.path.join(target_dir_path, clean_filename)
+
+        with open(target_path, 'wb') as fp:
+            for chunk in model_file.chunks():
+                fp.write(chunk)
+
+        return target_path
+
+    def make_final_path(self, date=None, directory=None):
+        # fixme: os.path.isdir test can be avoided in py37k
+        if date and directory:
+            raise ValueError("date and directory arguments are mutually exclusive")
+
+        data_dir_path = self.data_dir_path
+
+        if date:
+            final_path = data_dir_path
+            path_items = [date.strftime('%Y'), date.strftime('%m'), date.strftime('%d')]
+            for path_item in path_items:
+                final_path = os.path.join(final_path, six.text_type(path_item))
+                if not os.path.isdir(final_path):
+                    os.mkdir(final_path)
+            return final_path
+
+        if directory:
+            final_path = os.path.join(data_dir_path, six.text_type(directory))
+            if not os.path.isdir(final_path):
+                os.mkdir(final_path)
+            return final_path
+
+        return data_dir_path
+
+
+def get_tmp_filename(user):
+    filename_bits = [
+        user.slug,
+        timezone.now().strftime('%Y%m%d-%H%M%S'),
+        get_random_string(6),
+    ]
+
+    return '-'.join(filename_bits)
+
+
+def trim_long_filename(filename):
+    # fixme: consider moving this utility to better place?
+    # eg. to trim too long attachment filenames on upload
+    if len(filename) < FILENAME_MAX_LEN:
+        return filename
+
+    name, extension = os.path.splitext(filename)
+    name_len = FILENAME_MAX_LEN - len(extension)
+    return u'{}{}'.format(name[:name_len], extension)

+ 0 - 0
misago/users/forms/__init__.py


+ 94 - 9
misago/users/forms.py → misago/users/forms/admin.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.db.models import Q
 from django.contrib.auth import get_user_model
 from django.contrib.auth.password_validation import validate_password
 from django.utils.translation import ugettext_lazy as _
@@ -9,8 +10,9 @@ from misago.conf import settings
 from misago.core import threadstore
 from misago.core.forms import IsoDateTimeField, YesNoSwitch
 from misago.core.validators import validate_sluggable
-from misago.users.models import Ban, Rank
+from misago.users.models import Ban, DataDownload, Rank
 from misago.users.profilefields import profilefields
+from misago.users.utils import hash_email
 from misago.users.validators import validate_email, validate_username
 
 
@@ -460,14 +462,7 @@ class BanUsersForm(forms.Form):
     ban_type = forms.MultipleChoiceField(
         label=_("Values to ban"),
         widget=forms.CheckboxSelectMultiple,
-        choices=[
-            ('usernames', _('Usernames')),
-            ('emails', _('E-mails')),
-            ('domains', _('E-mail domains')),
-            ('ip', _('IP addresses')),
-            ('ip_first', _('First segment of IP addresses')),
-            ('ip_two', _('First two segments of IP addresses')),
-        ]
+        choices=[]
     )
     user_message = forms.CharField(
         label=_("User message"),
@@ -495,6 +490,25 @@ class BanUsersForm(forms.Form):
         help_text=_("Leave this field empty for set bans to never expire.")
     )
 
+    def __init__(self, *args, **kwargs):
+        users = kwargs.pop('users')
+
+        super(BanUsersForm, self).__init__(*args, **kwargs)
+
+        self.fields['ban_type'].choices = [
+            ('usernames', _('Usernames')),
+            ('emails', _('E-mails')),
+            ('domains', _('E-mail domains')),
+        ]
+
+        enable_ip_bans = list(filter(None, [u.joined_from_ip for u in users]))
+        if enable_ip_bans:
+            self.fields['ban_type'].choices += [
+                ('ip', _('IP addresses')),
+                ('ip_first', _('First segment of IP addresses')),
+                ('ip_two', _('First two segments of IP addresses')),
+            ]
+
 
 class BanForm(forms.ModelForm):
     check_type = forms.TypedChoiceField(
@@ -628,3 +642,74 @@ class SearchBansForm(forms.Form):
             queryset = queryset.filter(registration_only=False)
 
         return queryset
+
+
+class RequestDataDownloadsForm(forms.Form):
+    user_identifiers = forms.CharField(
+        label=_("Usernames or emails"),
+        help_text=_(
+            "Enter every item in new line. Duplicates will be ignored. "
+            "This field is case insensitive. Depending on site configuration and amount of data "
+            "to archive it may take up to few days for requests to complete. E-mail "
+            "will notification will be sent to every user once their download is ready."
+        ),
+        widget=forms.Textarea,
+    )
+
+    def clean_user_identifiers(self):
+        user_identifiers = self.cleaned_data['user_identifiers'].lower().splitlines()
+        user_identifiers = list(filter(bool, user_identifiers))
+        user_identifiers = list(set(user_identifiers))
+        
+        if len(user_identifiers) > 20:
+            raise forms.ValidationError(
+                _(
+                    "You may not enter more than 20 items at single time "
+                    "(You have entered %(show_value)s)."
+                ) % {'show_value': len(user_identifiers)}
+            )
+        
+        return user_identifiers
+
+    def clean(self):
+        data = super(RequestDataDownloadsForm, self).clean()
+
+        if data.get('user_identifiers'):
+            username_match = Q(slug__in=data['user_identifiers'])
+            email_match = Q(email_hash__in=map(hash_email, data['user_identifiers']))
+
+            data['users'] = list(UserModel.objects.filter(username_match | email_match))
+
+            if len(data['users']) != len(data['user_identifiers']):
+                raise forms.ValidationError(_("One or more specified users could not be found."))
+
+        return data
+
+
+class SearchDataDownloadsForm(forms.Form):
+    status = forms.ChoiceField(
+        label=_("Status"),
+        required=False,
+        choices=DataDownload.STATUS_CHOICES,
+    )
+    user = forms.CharField(
+        label=_("User"),
+        required=False,
+    )
+    requested_by = forms.CharField(
+        label=_("Requested by"),
+        required=False,
+    )
+
+    def filter_queryset(self, search_criteria, queryset):
+        criteria = search_criteria
+        if criteria.get('status') is not None:
+            queryset = queryset.filter(status=criteria['status'])
+
+        if criteria.get('user'):
+            queryset = queryset.filter(user__slug__istartswith=criteria['user'])
+
+        if criteria.get('requested_by'):
+            queryset = queryset.filter(requester__slug__istartswith=criteria['requested_by'])
+
+        return queryset

+ 15 - 5
misago/users/forms/register.py

@@ -12,7 +12,14 @@ UserModel = get_user_model()
 
 
 class BaseRegisterForm(forms.Form):
+    username = forms.CharField(validators=[validators.validate_username])
+    email = forms.CharField(validators=[validators.validate_email])
+
+    terms_of_service = forms.IntegerField(required=False)
+    privacy_policy = forms.IntegerField(required=False)
+
     def __init__(self, *args, **kwargs):
+        self.agreements = kwargs.pop('agreements')
         self.request = kwargs.pop('request')
         super(BaseRegisterForm, self).__init__(*args, **kwargs)
 
@@ -38,6 +45,12 @@ class BaseRegisterForm(forms.Form):
                 raise ValidationError(_("This e-mail address is not allowed."))
         return data
 
+    def clean_agreements(self, data):
+        for field_name, agreement in self.agreements.items():
+            if data.get(field_name) != agreement['id']:
+                error = ValueError(_("This agreement is required."))
+                self.add_error(field_name, error)
+
     def raise_if_ip_banned(self):
         ban = get_ip_ban(self.request.user_ip, registration_only=True)
         if ban:
@@ -48,12 +61,10 @@ class BaseRegisterForm(forms.Form):
 
 
 class SocialAuthRegisterForm(BaseRegisterForm):
-    username = forms.CharField(validators=[validators.validate_username])
-    email = forms.CharField(validators=[validators.validate_email])
-
     def clean(self):
         cleaned_data = super(SocialAuthRegisterForm, self).clean()
 
+        self.clean_agreements(cleaned_data)
         self.raise_if_ip_banned()
 
         validators.validate_new_registration(self.request, cleaned_data, self)
@@ -62,8 +73,6 @@ class SocialAuthRegisterForm(BaseRegisterForm):
 
 
 class RegisterForm(BaseRegisterForm):
-    username = forms.CharField(validators=[validators.validate_username])
-    email = forms.CharField(validators=[validators.validate_email])
     password = forms.CharField(strip=False)
 
     # placeholder field for setting captcha errors on form
@@ -82,6 +91,7 @@ class RegisterForm(BaseRegisterForm):
     def clean(self):
         cleaned_data = super(RegisterForm, self).clean()
 
+        self.clean_agreements(cleaned_data)
         self.raise_if_ip_banned()
 
         try:

+ 2 - 2
misago/users/management/commands/createsuperuser.py

@@ -166,8 +166,8 @@ class Command(BaseCommand):
             )
 
             if verbosity >= 1:
-                message = "Superuser #%(pk)s has been created successfully."
-                self.stdout.write(message % {'pk': user.pk})
+                message = "Superuser #{pk} has been created successfully."
+                self.stdout.write(message.format(pk=user.pk))
         except ValidationError as e:
             self.stderr.write(e.messages[0])
         except IntegrityError as e:

+ 41 - 0
misago/users/management/commands/deleteinactiveusers.py

@@ -0,0 +1,41 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+
+from misago.conf import settings
+from misago.core.pgutils import chunk_queryset
+
+
+UserModel = get_user_model()
+
+
+class Command(BaseCommand):
+    help = (
+        "Deletes inactive user accounts older than set time."
+    )
+
+    def handle(self, *args, **options):
+        if not settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS:
+            self.stdout.write("Automatic deletion of inactive users is currently disabled.")
+            return
+
+
+        users_deleted = 0
+        
+        joined_on_cutoff = timezone.now() - timedelta(
+            days=settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS)
+
+        queryset = UserModel.objects.filter(
+            requires_activation__gt=UserModel.ACTIVATION_NONE,
+            joined_on__lt=joined_on_cutoff,
+        )
+
+        for user in chunk_queryset(queryset):
+            user.delete()
+            users_deleted += 1
+
+        self.stdout.write("Deleted users: {}".format(users_deleted))

+ 24 - 0
misago/users/management/commands/expireuserdatadownloads.py

@@ -0,0 +1,24 @@
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+
+from misago.core.pgutils import chunk_queryset
+from misago.users.datadownloads import expire_user_data_download
+from misago.users.models import DataDownload
+
+
+class Command(BaseCommand):
+    help = "Expires old user data downloads."
+
+    def handle(self, *args, **options):
+        downloads_expired = 0
+        queryset = DataDownload.objects.select_related('user')
+        queryset = queryset.filter(
+            status=DataDownload.STATUS_READY,
+            expires_on__lte=timezone.now(),
+        )
+
+        for data_download in chunk_queryset(queryset):
+            expire_user_data_download(data_download)
+            downloads_expired += 1
+
+        self.stdout.write("Data downloads expired: {}".format(downloads_expired))

+ 2 - 2
misago/users/management/commands/invalidatebans.py

@@ -20,7 +20,7 @@ class Command(BaseCommand):
         queryset = queryset.filter(expires_on__lt=timezone.now())
 
         expired_count = queryset.update(is_checked=False)
-        self.stdout.write("Bans invalidated: %s" % expired_count)
+        self.stdout.write("Bans invalidated: {}".format(expired_count))
 
     def handle_bans_caches(self):
         queryset = BanCache.objects.filter(expires_on__lt=timezone.now())
@@ -34,4 +34,4 @@ class Command(BaseCommand):
         expired_count += queryset.count()
         queryset.delete()
 
-        self.stdout.write("Ban caches emptied: %s" % expired_count)
+        self.stdout.write("Ban caches emptied: {}".format(expired_count))

+ 2 - 6
misago/users/management/commands/populateonlinetracker.py

@@ -15,11 +15,7 @@ class Command(BaseCommand):
         entries_created = 0
         queryset = UserModel.objects.filter(online_tracker__isnull=True)
         for user in chunk_queryset(queryset):
-            Online.objects.create(
-                user=user,
-                current_ip=user.joined_from_ip,
-                last_click=user.last_login,
-            )
+            Online.objects.create(user=user, last_click=user.last_login)
             entries_created += 1
 
-        self.stdout.write("Tracker entries created: %s" % entries_created)
+        self.stdout.write("Tracker entries created: {}".format(entries_created))

+ 42 - 0
misago/users/management/commands/prepareuserdatadownloads.py

@@ -0,0 +1,42 @@
+import logging
+
+from django.core.management.base import BaseCommand
+from django.utils.translation import ugettext
+
+from misago.conf import settings
+from misago.core.mail import mail_user
+from misago.core.pgutils import chunk_queryset
+from misago.users.datadownloads import prepare_user_data_download
+from misago.users.models import DataDownload
+
+
+logger = logging.getLogger('misago.users.datadownloads')
+
+
+class Command(BaseCommand):
+    help = "Prepares user data downloads."
+    leave_locale_alone = True
+
+    def handle(self, *args, **options):
+        working_dir = settings.MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR
+        if not working_dir:
+            self.stdout.write(
+                "MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR has to be set in order for "
+                "this feature to work.")
+            return
+        
+        downloads_prepared = 0
+        queryset = DataDownload.objects.select_related('user')
+        queryset = queryset.filter(status=DataDownload.STATUS_PENDING)
+        for data_download in chunk_queryset(queryset):
+            if prepare_user_data_download(data_download, logger):
+                user = data_download.user
+                subject = ugettext("%(user)s, your data download is ready") % { 'user': user }
+                mail_user(user, subject, 'misago/emails/data_download', context={
+                    'data_download': data_download,
+                    'expires_in': settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS,
+                })
+
+                downloads_prepared += 1
+
+        self.stdout.write("Data downloads prepared: {}".format(downloads_prepared))

+ 18 - 0
misago/users/management/commands/removeoldips.py

@@ -0,0 +1,18 @@
+from django.core.management import BaseCommand
+
+from misago.conf import settings
+from misago.users.signals import remove_old_ips
+
+
+class Command(BaseCommand):
+    help = "Removes users IPs stored for longer than set in MISAGO_IP_STORE_TIME."
+    
+    def handle(self, *args, **options):
+        if not settings.MISAGO_IP_STORE_TIME:
+            self.stdout.write("Old IP removal is disabled.")
+            return
+
+        remove_old_ips.send(sender=self)
+
+        self.stdout.write(
+            "IP addresses older than {} days have been removed.".format(settings.MISAGO_IP_STORE_TIME))

+ 3 - 3
misago/users/management/commands/synchronizeusers.py

@@ -25,12 +25,12 @@ class Command(BaseCommand):
     def sync_users(self, users_to_sync):
         categories = Category.objects.root_category().get_descendants()
 
-        message = "Synchronizing %s users...\n"
-        self.stdout.write(message % users_to_sync)
+        self.stdout.write("Synchronizing {} users...\n".format(users_to_sync))
 
         synchronized_count = 0
         show_progress(self, synchronized_count, users_to_sync)
         start_time = time.time()
+        
         for user in chunk_queryset(UserModel.objects.all()):
             user.threads = user.thread_set.filter(
                 category__in=categories,
@@ -52,4 +52,4 @@ class Command(BaseCommand):
             synchronized_count += 1
             show_progress(self, synchronized_count, users_to_sync, start_time)
 
-        self.stdout.write("\n\nSynchronized %s users" % synchronized_count)
+        self.stdout.write("\n\nSynchronized {} users".format(synchronized_count))

+ 17 - 3
misago/users/middleware.py

@@ -2,7 +2,7 @@ from django.contrib.auth import logout
 from django.utils.deprecation import MiddlewareMixin
 
 from .bans import get_request_ip_ban, get_user_ban
-from .models import AnonymousUser
+from .models import AnonymousUser, Online
 from .online import tracker
 
 
@@ -27,8 +27,22 @@ class UserMiddleware(MiddlewareMixin):
 
 class OnlineTrackerMiddleware(MiddlewareMixin):
     def process_request(self, request):
-        tracker.start_request_tracker(request)
+        if request.user.is_authenticated:
+            try:
+                request._misago_online_tracker = request.user.online_tracker
+            except Online.DoesNotExist:
+                tracker.start_tracking(request, request.user)
+        else:
+            request._misago_online_tracker = None
 
     def process_response(self, request, response):
-        tracker.update_request_tracker(request)
+        if hasattr(request, '_misago_online_tracker'):
+            online_tracker = request._misago_online_tracker
+
+            if online_tracker:
+                if request.user.is_anonymous:
+                    tracker.stop_tracking(request, online_tracker)
+                else:
+                    tracker.update_tracker(request, online_tracker)
+
         return response

+ 33 - 0
misago/users/migrations/0012_audittrail.py

@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-03 18:46
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('misago_users', '0011_auto_20180331_2208'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AuditTrail',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('object_id', models.PositiveIntegerField()),
+                ('created_on', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
+                ('ip_address', models.GenericIPAddressField()),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-pk'],
+            },
+        ),
+    ]

+ 28 - 0
misago/users/migrations/0013_auto_20180609_1523.py

@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-09 15:23
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_users', '0012_audittrail'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='online',
+            name='current_ip',
+        ),
+        migrations.RemoveField(
+            model_name='user',
+            name='last_ip',
+        ),
+        migrations.AlterField(
+            model_name='user',
+            name='joined_from_ip',
+            field=models.GenericIPAddressField(blank=True, null=True),
+        ),
+    ]

+ 35 - 0
misago/users/migrations/0014_datadownload.py

@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-24 00:13
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import misago.users.models.datadownload
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_users', '0013_auto_20180609_1523'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='DataDownload',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('status', models.PositiveIntegerField(choices=[(0, 'Pending'), (1, 'Processing'), (2, 'Ready'), (3, 'Expired')], db_index=True, default=0)),
+                ('requester_name', models.CharField(max_length=255)),
+                ('requested_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('expires_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('file', models.FileField(blank=True, max_length=255, null=True, upload_to=misago.users.models.datadownload.get_data_upload_to)),
+                ('requester', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-pk'],
+            },
+        ),
+    ]

+ 21 - 0
misago/users/migrations/0015_user_agreements.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.15 on 2018-08-16 17:29
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_users', '0014_datadownload'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='agreements',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), default=list, size=None),
+        ),
+    ]

+ 2 - 0
misago/users/models/__init__.py

@@ -2,5 +2,7 @@ from .rank import Rank
 from .user import AnonymousUser, Online, User, UsernameChange
 from .activityranking import ActivityRanking
 from .avatar import Avatar
+from .audittrail import AuditTrail
 from .avatargallery import AvatarGallery
 from .ban import Ban, BanCache
+from .datadownload import DataDownload

+ 18 - 0
misago/users/models/audittrail.py

@@ -0,0 +1,18 @@
+from django.conf import settings
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.utils import timezone
+
+
+class AuditTrail(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+    created_on = models.DateTimeField(db_index=True, default=timezone.now)
+    ip_address = models.GenericIPAddressField()
+
+    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+    object_id = models.PositiveIntegerField()
+    content_object = GenericForeignKey('content_type', 'object_id')
+
+    class Meta:
+        ordering = ['-pk']

+ 0 - 3
misago/users/models/avatargallery.py

@@ -10,9 +10,6 @@ class AvatarGallery(models.Model):
     class Meta:
         ordering = ['gallery', 'pk']
 
-    @property
-    def path(self):
-        return self.image.path
 
     @property
     def url(self):

+ 53 - 0
misago/users/models/datadownload.py

@@ -0,0 +1,53 @@
+from hashlib import md5
+
+from django.conf import settings
+from django.db import models
+from django.utils import timezone
+from django.utils.crypto import get_random_string
+from django.utils.translation import ugettext_lazy as _
+
+
+def get_data_upload_to(instance, filename):
+    user_id_hexdigest = md5(str(instance.user_id).encode()).hexdigest()
+    return 'data-downloads/{}/{}/{}.zip'.format(
+        user_id_hexdigest, get_random_string(64), instance.user.slug)
+
+
+class DataDownload(models.Model):
+    STATUS_PENDING = 0
+    STATUS_PROCESSING = 1
+    STATUS_READY = 2
+    STATUS_EXPIRED = 3
+
+    STATUS_CHOICES = [
+        (STATUS_PENDING, _("Pending")),
+        (STATUS_PROCESSING, _("Processing")),
+        (STATUS_READY, _("Ready")),
+        (STATUS_EXPIRED, _("Expired")),
+    ]
+
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
+    status = models.PositiveIntegerField(
+        default=STATUS_PENDING,
+        choices=STATUS_CHOICES,
+        db_index=True,
+    )
+    requester = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+    )
+    requester_name = models.CharField(max_length=255)
+    requested_on = models.DateTimeField(default=timezone.now)
+    expires_on = models.DateTimeField(default=timezone.now)
+    file = models.FileField(upload_to=get_data_upload_to, max_length=255, null=True, blank=True)
+
+    class Meta:
+        ordering = ['-pk']
+
+    def delete(self, *args, **kwargs):
+        if self.file:
+            self.file.delete(save=False)
+        super(DataDownload, self).delete(*args, **kwargs)

+ 21 - 20
misago/users/models/user.py

@@ -4,7 +4,7 @@ from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
 from django.contrib.auth.models import UserManager as BaseUserManager
 from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
 from django.contrib.auth.password_validation import validate_password
-from django.contrib.postgres.fields import HStoreField, JSONField
+from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
 from django.core.mail import send_mail
 from django.db import IntegrityError, models, transaction
 from django.urls import reverse
@@ -17,6 +17,7 @@ from misago.conf import settings
 from misago.core.pgutils import PgPartialIndex
 from misago.core.utils import slugify
 from misago.users import avatars
+from misago.users.audittrail import create_user_audit_trail
 from misago.users.signatures import is_user_signature_valid
 from misago.users.utils import hash_email
 
@@ -26,8 +27,8 @@ from .rank import Rank
 class UserManager(BaseUserManager):
     @transaction.atomic
     def create_user(
-            self, username, email, password=None, set_default_avatar=False, **extra_fields
-    ):
+            self, username, email, password=None, create_audit_trail=False,
+            joined_from_ip=None, set_default_avatar=False, **extra_fields):
         from misago.users.validators import validate_email, validate_username
 
         email = self.normalize_email(email)
@@ -36,9 +37,6 @@ class UserManager(BaseUserManager):
         if not email:
             raise ValueError(_("User must have an email address."))
 
-        if not 'joined_from_ip' in extra_fields:
-            extra_fields['joined_from_ip'] = '127.0.0.1'
-
         WATCH_DICT = {
             'no': self.model.SUBSCRIBE_NONE,
             'watch': self.model.SUBSCRIBE_NOTIFY,
@@ -56,7 +54,12 @@ class UserManager(BaseUserManager):
         extra_fields.update({'is_staff': False, 'is_superuser': False})
 
         now = timezone.now()
-        user = self.model(last_login=now, joined_on=now, **extra_fields)
+        user = self.model(
+            last_login=now, 
+            joined_on=now, 
+            joined_from_ip=joined_from_ip,
+            **extra_fields
+        )
 
         user.set_username(username)
         user.set_email(email)
@@ -89,12 +92,11 @@ class UserManager(BaseUserManager):
 
         user.save(update_fields=['avatars', 'acl_key'])
 
+        if create_audit_trail:
+            create_user_audit_trail(user, user.joined_from_ip, user)
+
         # populate online tracker with default value
-        Online.objects.create(
-            user=user,
-            current_ip=extra_fields['joined_from_ip'],
-            last_click=now,
-        )
+        Online.objects.create(user=user, last_click=now)
 
         return user
 
@@ -173,8 +175,7 @@ class User(AbstractBaseUser, PermissionsMixin):
     email_hash = models.CharField(max_length=32, unique=True)
 
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
-    joined_from_ip = models.GenericIPAddressField()
-    last_ip = models.GenericIPAddressField(null=True, blank=True)
+    joined_from_ip = models.GenericIPAddressField(null=True, blank=True)
     is_hiding_presence = models.BooleanField(default=False)
 
     rank = models.ForeignKey(
@@ -269,6 +270,7 @@ class User(AbstractBaseUser, PermissionsMixin):
     last_posted_on = models.DateTimeField(null=True, blank=True)
 
     profile_fields = HStoreField(default=dict)
+    agreements = ArrayField(models.PositiveIntegerField(), default=list)
 
     USERNAME_FIELD = 'slug'
     REQUIRED_FIELDS = ['email']
@@ -303,7 +305,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         if kwargs.pop('delete_content', False):
             self.delete_content()
 
-        self.anonymize_content()
+        self.anonymize_data()
 
         avatars.delete_avatar(self)
 
@@ -318,7 +320,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.is_deleting_account = True
         self.save(update_fields=['is_active', 'is_deleting_account'])
 
-    def anonymize_content(self):
+    def anonymize_data(self):
         """Replaces username with anonymized one, then send anonymization signal.
 
         Items associated with this user then anonymize their user-specific data
@@ -327,8 +329,8 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.username = settings.MISAGO_ANONYMOUS_USERNAME
         self.slug = slugify(self.username)
         
-        from misago.users.signals import anonymize_user_content
-        anonymize_user_content.send(sender=self)
+        from misago.users.signals import anonymize_user_data
+        anonymize_user_data.send(sender=self)
 
     @property
     def acl_cache(self):
@@ -477,7 +479,6 @@ class Online(models.Model):
         related_name='online_tracker',
         on_delete=models.CASCADE,
     )
-    current_ip = models.GenericIPAddressField()
     last_click = models.DateTimeField(default=timezone.now)
 
     def save(self, *args, **kwargs):
@@ -506,7 +507,7 @@ class UsernameChange(models.Model):
     old_username = models.CharField(max_length=255)
 
     class Meta:
-        get_latest_by = 'changed_on'
+        get_latest_by = "changed_on"
 
     def set_change_author(self, user):
         self.changed_by = user

+ 27 - 24
misago/users/namechanges.py

@@ -1,3 +1,6 @@
+"""
+Service for tracking namechanges
+"""
 from datetime import timedelta
 
 from django.utils import timezone
@@ -5,32 +8,32 @@ from django.utils import timezone
 from .models import UsernameChange
 
 
-def get_available_namechanges_data(user):
-    namechanges_data = {
-        'changes_left': 0,
-        'next_change_on': None,
-    }
+class UsernameChanges(object):
+    def __init__(self, user):
+        self.left = 0
+        self.next_on = None
 
-    if not user.acl_cache['name_changes_allowed']:
-        return namechanges_data
+        if user.acl_cache['name_changes_allowed']:
+            self.count_namechanges(user)
 
-    name_changes_allowed = user.acl_cache['name_changes_allowed']
-    name_changes_expire = user.acl_cache['name_changes_expire']
+    def count_namechanges(self, user):
+        name_changes_allowed = user.acl_cache['name_changes_allowed']
+        name_changes_expire = user.acl_cache['name_changes_expire']
 
-    valid_changes_qs = user.namechanges.filter(changed_by=user)
-    if name_changes_expire:
-        cutoff = timezone.now() - timedelta(days=name_changes_expire)
-        valid_changes_qs = valid_changes_qs.filter(changed_on__gte=cutoff)
+        valid_changes_qs = user.namechanges.filter(changed_by=user)
+        if name_changes_expire:
+            cutoff = timezone.now() - timedelta(days=name_changes_expire)
+            valid_changes_qs = valid_changes_qs.filter(changed_on__gte=cutoff)
 
-    used_changes = valid_changes_qs.count()
-    if name_changes_allowed > used_changes:
-        namechanges_data['changes_left'] = name_changes_allowed - used_changes
+        used_changes = valid_changes_qs.count()
+        if name_changes_allowed <= used_changes:
+            self.left = 0
+        else:
+            self.left = name_changes_allowed - used_changes
 
-    if not namechanges_data['changes_left'] and name_changes_expire:
-        try:
-            namechanges_data['next_change_on'] = valid_changes_qs.latest().changed_on
-            namechanges_data['next_change_on'] += timedelta(days=name_changes_expire)
-        except UsernameChange.DoesNotExist:
-            pass
-    
-    return namechanges_data
+        if not self.left and name_changes_expire:
+            try:
+                self.next_on = valid_changes_qs.latest().changed_on
+                self.next_on += timedelta(days=name_changes_expire)
+            except UsernameChange.DoesNotExist:
+                pass

+ 16 - 50
misago/users/online/tracker.py

@@ -5,66 +5,32 @@ from django.utils import timezone
 from misago.users.models import Online
 
 
-def unwrap_drf_request(f):
-    """utility decorator that unwraps django request from rest frameworks wrapper"""
-    def unwrapped_request_view(request, *args, **kwargs):
-        if isinstance(request, Request):
-            request = request._request
-        return f(request, *args, **kwargs)
-    return unwrapped_request_view
-
-
-@unwrap_drf_request
-def start_request_tracker(request):
-    if request.user.is_authenticated:
-        try:
-            request._misago_online_tracker = request.user.online_tracker
-        except Online.DoesNotExist:
-            start_tracking(request, request.user)
-    else:
-        request._misago_online_tracker = None
-    
-
-@unwrap_drf_request
-def update_request_tracker(request):
-    try:
-        online_tracker = request._misago_online_tracker
-    except AttributeError:
-        return
-
-    if online_tracker:
-        if request.user.is_anonymous:
-            stop_tracking(request, online_tracker)
-        else:
-            update_tracking(request, online_tracker)
-
-
-@unwrap_drf_request
-def clear_request_tracker(request):
+def mute_tracker(request):
     request._misago_online_tracker = None
 
 
 def start_tracking(request, user):
-    online_tracker = Online.objects.create(
-        user=user,
-        current_ip=request.user_ip,
-    )
+    online_tracker = Online.objects.create(user=user)
 
     request.user.online_tracker = online_tracker
     request._misago_online_tracker = online_tracker
 
 
-def update_tracking(request, online_tracker):
-    online_tracker.current_ip = request.user_ip
-    online_tracker.last_click = timezone.now()
+def update_tracker(request, tracker):
+    tracker.last_click = timezone.now()
+
+    tracker.save(update_fields=['last_click'])
 
-    online_tracker.save(update_fields=['last_click', 'current_ip'])
 
+def stop_tracking(request, tracker):
+    user = tracker.user
+    user.last_login = tracker.last_click
+    user.save(update_fields=['last_login'])
 
-def stop_tracking(request, online_tracker):
-    user = online_tracker.user
-    user.last_login = online_tracker.last_click
-    user.last_ip = online_tracker.current_ip
-    user.save(update_fields=['last_login', 'last_ip'])
+    tracker.delete()
 
-    online_tracker.delete()
+
+def clear_tracking(request):
+    if isinstance(request, Request):
+        request = request._request  # Fugly unwrap restframework's request 
+    request._misago_online_tracker = None

+ 2 - 13
misago/users/profilefields/default.py

@@ -89,20 +89,9 @@ class JoinIpField(basefields.TextProfileField):
         if not request.user.acl_cache.get('can_see_users_ips'):
             return None
 
-        return {
-            'text': user.joined_from_ip
-        }
-
-
-class LastIpField(basefields.TextProfileField):
-    fieldname = 'last_ip'
-    label = _("Last IP")
-    readonly = True
-
-    def get_value_display_data(self, request, user, value):
-        if not request.user.acl_cache.get('can_see_users_ips'):
+        if not user.joined_from_ip:
             return None
 
         return {
-            'text': user.last_ip or user.joined_from_ip
+            'text': user.joined_from_ip
         }

+ 32 - 14
misago/users/registration.py

@@ -2,6 +2,8 @@ from django.utils.translation import ugettext as _
 
 from misago.conf import settings
 from misago.core.mail import mail_user
+from misago.legal.models import Agreement
+from misago.legal.utils import save_user_agreement_acceptance
 from misago.users.tokens import make_activation_token
 
 
@@ -9,25 +11,41 @@ def send_welcome_email(request, user):
     mail_subject = _("Welcome on %(forum_name)s forums!")
     mail_subject = mail_subject % {'forum_name': settings.forum_name}
 
-    if user.requires_activation:
-        activation_token = make_activation_token(user)
+    if not user.requires_activation:
+        mail_user(user, mail_subject, 'misago/emails/register/complete')
+        return
 
-        activation_by_admin = user.requires_activation_by_admin
-        activation_by_user = user.requires_activation_by_user
+    activation_token = make_activation_token(user)
 
-        mail_user(
-            request, user, mail_subject, 'misago/emails/register/inactive', {
-                'activation_token': activation_token,
-                'activation_by_admin': activation_by_admin,
-                'activation_by_user': activation_by_user,
-            }
-        )
-    else:
-        mail_user(request, user, mail_subject, 'misago/emails/register/complete')
+    activation_by_admin = user.requires_activation_by_admin
+    activation_by_user = user.requires_activation_by_user
+
+    mail_user(
+        user,
+        mail_subject,
+        'misago/emails/register/inactive',
+        context={
+            'activation_token': activation_token,
+            'activation_by_admin': activation_by_admin,
+            'activation_by_user': activation_by_user,
+        }
+    )
+
+
+def save_user_agreements(user, form):
+    if not form.agreements:
+        return
+
+    for field_name in form.agreements.keys():
+        agreement_id = form.cleaned_data[field_name]
+        agreement = Agreement.objects.get(id=agreement_id)
+        save_user_agreement_acceptance(user, agreement)
+
+    user.save(update_fields=['agreements'])
 
 
 def get_registration_result_json(user):
-    activation_method = None
+    activation_method = 'active'
     if user.requires_activation_by_admin:
         activation_method = 'admin'
     elif user.requires_activation_by_user:

+ 8 - 14
misago/users/serializers/__init__.py

@@ -1,14 +1,8 @@
-from .ban import BanMessageSerializer, BanDetailsSerializer
-from .moderation import ModerateAvatarSerializer, ModerateSignatureSerializer
-from .options import (
-    ForumOptionsSerializer, EditSignatureSerializer, ChangeUsernameSerializer,
-    ChangePasswordSerializer, ChangeEmailSerializer, DeleteOwnAccountSerializer,
-)
-from .rank import RankSerializer
-from .user import StatusSerializer, UserCardSerializer, UserSerializer
-from .auth import (
-    AuthenticatedUserSerializer, AnonymousUserSerializer, LoginSerializer,
-    ResendActivationSerializer, SendPasswordFormSerializer, ChangeForgottenPasswordSerializer,
-)
-from .register import RegisterUserSerializer
-from .usernamechange import UsernameChangeSerializer
+from .ban import *
+from .datadownload import *
+from .moderation import *
+from .options import *
+from .rank import *
+from .user import *
+from .auth import *
+from .usernamechange import *

+ 45 - 97
misago/users/serializers/auth.py

@@ -1,23 +1,33 @@
 from rest_framework import serializers
 
 from django.contrib.auth import get_user_model
-from django.contrib.auth.password_validation import validate_password
-from django.core.exceptions import ValidationError
 from django.urls import reverse
-from django.utils.translation import ugettext_lazy, ugettext as _
 
 from misago.acl import serialize_acl
-from misago.users.authmixin import AuthMixin
-from misago.users.tokens import is_password_change_token_valid
 
 from .user import UserSerializer
 
 
 UserModel = get_user_model()
 
+__all__ = [
+    'AuthenticatedUserSerializer',
+    'AnonymousUserSerializer',
+]
 
-class AuthenticatedUserSerializer(UserSerializer):
+
+class AuthFlags(object):
+    def get_is_authenticated(self, obj):
+        return bool(obj.is_authenticated)
+
+    def get_is_anonymous(self, obj):
+        return bool(obj.is_anonymous)
+
+
+class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
     email = serializers.SerializerMethodField()
+    is_authenticated = serializers.SerializerMethodField()
+    is_anonymous = serializers.SerializerMethodField()
 
     class Meta:
         model = UserModel
@@ -28,11 +38,30 @@ class AuthenticatedUserSerializer(UserSerializer):
             'unread_private_threads',
             'subscribe_to_started_threads',
             'subscribe_to_replied_threads',
+            'is_authenticated',
+            'is_anonymous',
         ]
 
+    def get_acl(self, obj):
+        return serialize_acl(obj)
+
     def get_email(self, obj):
         return obj.email
 
+    def get_api(self, obj):
+        return {
+            'avatar': reverse('misago:api:user-avatar', kwargs={'pk': obj.pk}),
+            'data_downloads': reverse('misago:api:user-data-downloads', kwargs={'pk': obj.pk}),
+            'details': reverse('misago:api:user-details', kwargs={'pk': obj.pk}),
+            'change_email': reverse('misago:api:user-change-email', kwargs={'pk': obj.pk}),
+            'change_password': reverse('misago:api:user-change-password', kwargs={'pk': obj.pk}),
+            'edit_details': reverse('misago:api:user-edit-details', kwargs={'pk': obj.pk}),
+            'options': reverse('misago:api:user-forum-options', kwargs={'pk': obj.pk}),
+            'request_data_download': reverse('misago:api:user-request-data-download', kwargs={'pk': obj.pk}),
+            'username': reverse('misago:api:user-username', kwargs={'pk': obj.pk}),
+            'delete': reverse('misago:api:user-delete-own-account', kwargs={'pk': obj.pk}),
+        }
+
 
 AuthenticatedUserSerializer = AuthenticatedUserSerializer.exclude_fields(
     'is_avatar_locked',
@@ -45,95 +74,14 @@ AuthenticatedUserSerializer = AuthenticatedUserSerializer.exclude_fields(
 )
 
 
-class AnonymousUserSerializer(serializers.Serializer):
+class AnonymousUserSerializer(serializers.Serializer, AuthFlags):
     id = serializers.ReadOnlyField()
-
-
-class LoginSerializer(serializers.Serializer, AuthMixin):
-    username = serializers.CharField(max_length=255)
-    password = serializers.CharField(max_length=255, trim_whitespace=False)
-
-    def validate(self, data):
-        user = self.authenticate(data.get('username'), data.get('password'))
-        self.confirm_login_allowed(user)
-        return {'user': user}
-
-
-class GetUserSerializer(serializers.Serializer, AuthMixin):
-    email = serializers.EmailField(max_length=255)
-
-    def validate(self, data):
-        user = self.get_user_by_email(data.get('email'))
-        self.confirm_allowed(user)
-        return {'user': user}
-
-    def confirm_allowed(self, user):
-        """override this method to include additional checks"""
-        pass
-
-
-class ResendActivationSerializer(GetUserSerializer):
-    def confirm_allowed(self, user):
-        username_format = {'user': user.username}
-        if not user.requires_activation:
-            message = _("%(user)s, your account is already active.")
-            raise ValidationError(message % username_format)
-        if user.requires_activation_by_admin:
-            message = _("%(user)s, only administrator may activate your account.")
-            raise ValidationError(message % username_format)
-
-
-class SendPasswordFormSerializer(GetUserSerializer):
-    auth_messages = {
-        'inactive_user': ugettext_lazy(
-            "You have to activate your account before "
-            "you will be able to request new password."
-        ),
-        'inactive_admin': ugettext_lazy(
-            "Administrator has to activate your account before "
-            "you will be able to request new password."
-        ),
-    }
-
-    def confirm_allowed(self, user):
-        self.confirm_user_active(user)
-
-
-class ChangeForgottenPasswordSerializer(serializers.Serializer, AuthMixin):
-    password = serializers.CharField(
-        max_length=255,
-        trim_whitespace=False,
-    )
-    token = serializers.CharField(max_length=255)
-
-    auth_messages = {
-        'inactive_user': ugettext_lazy(
-            "You have to activate your account before "
-            "you will be able to change your password."
-        ),
-        'inactive_admin': ugettext_lazy(
-            "Administrator has to activate your account before "
-            "you will be able to change your password."
-        ),
-    }
-
-    def confirm_allowed(self):
-        self.confirm_user_active(self.instance)
-        self.confirm_user_not_banned(self.instance)
-
-    def validate_password(self, value):
-        validate_password(value, user=self.instance)
-        return value
-
-    def validate_token(self, value):
-        if not is_password_change_token_valid(self.instance, value):
-            raise ValidationError(_("Form link is invalid or expired. Please try again."))
-        return value
-
-    def validate(self, data):
-        self.confirm_allowed()
-        return data
-
-    def save(self):
-        self.instance.set_password(self.validated_data['password'])
-        self.instance.save()
+    acl = serializers.SerializerMethodField()
+    is_authenticated = serializers.SerializerMethodField()
+    is_anonymous = serializers.SerializerMethodField()
+
+    def get_acl(self, obj):
+        if hasattr(obj, 'acl_cache'):
+            return serialize_acl(obj)
+        else:
+            return {}

+ 9 - 3
misago/users/serializers/ban.py

@@ -6,6 +6,12 @@ from misago.core.utils import format_plaintext_for_html
 from misago.users.models import Ban
 
 
+__all__ = [
+    'BanMessageSerializer',
+    'BanDetailsSerializer',
+]
+
+
 def serialize_message(message):
     if message:
         return {
@@ -17,16 +23,16 @@ def serialize_message(message):
 
 
 class BanMessageSerializer(serializers.ModelSerializer):
-    detail = serializers.SerializerMethodField()
+    message = serializers.SerializerMethodField()
 
     class Meta:
         model = Ban
         fields = [
-            'detail',
+            'message',
             'expires_on',
         ]
 
-    def get_detail(self, obj):
+    def get_message(self, obj):
         if obj.user_message:
             message = obj.user_message
         elif obj.check_type == Ban.IP:

+ 18 - 0
misago/users/serializers/datadownload.py

@@ -0,0 +1,18 @@
+from rest_framework import serializers
+
+from misago.users.models import DataDownload
+
+
+__all__ = ['DataDownloadSerializer']
+
+
+class DataDownloadSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = DataDownload
+        fields = [
+            'id',
+            'status',
+            'requested_on',
+            'expires_on',
+            'file',
+        ]

+ 5 - 0
misago/users/serializers/moderation.py

@@ -8,6 +8,11 @@ from misago.conf import settings
 
 UserModel = get_user_model()
 
+__all__ = [
+    'ModerateAvatarSerializer',
+    'ModerateSignatureSerializer',
+]
+
 
 class ModerateAvatarSerializer(serializers.ModelSerializer):
     class Meta:

+ 21 - 5
misago/users/serializers/options.py

@@ -5,13 +5,22 @@ from django.contrib.auth.password_validation import validate_password
 from django.utils.translation import ugettext as _
 
 from misago.conf import settings
-from misago.users.online.tracker import clear_request_tracker
+from misago.users.online.tracker import clear_tracking
 from misago.users.permissions import allow_delete_own_account
 from misago.users.validators import validate_email, validate_username
 
 
 UserModel = get_user_model()
 
+__all__ = [
+    'ForumOptionsSerializer',
+    'EditSignatureSerializer',
+    'ChangeUsernameSerializer',
+    'ChangePasswordSerializer',
+    'ChangeEmailSerializer',
+    'DeleteOwnAccountSerializer',
+]
+
 
 class ForumOptionsSerializer(serializers.ModelSerializer):
     class Meta:
@@ -46,13 +55,20 @@ class EditSignatureSerializer(serializers.ModelSerializer):
 
 
 class ChangeUsernameSerializer(serializers.Serializer):
-    username = serializers.CharField(max_length=200, required=True, allow_blank=False)
+    username = serializers.CharField(max_length=200, required=False, allow_blank=True)
+
+    def validate(self, data):
+        username = data.get('username')
+
+        if not username:
+            raise serializers.ValidationError(_("Enter new username."))
 
-    def validate_username(self, username):
         if username == self.context['user'].username:
             raise serializers.ValidationError(_("New username is same as current one."))
+
         validate_username(username)
-        return username
+
+        return data
 
     def change_username(self, changed_by):
         self.context['user'].set_username(self.validated_data['username'], changed_by=changed_by)
@@ -111,6 +127,6 @@ class DeleteOwnAccountSerializer(serializers.Serializer):
         allow_delete_own_account(request.user, profile)
         
         logout(request)
-        clear_request_tracker(request)
+        clear_tracking(request)
 
         profile.mark_for_delete()

+ 8 - 0
misago/users/serializers/rank.py

@@ -4,8 +4,12 @@ from misago.core.utils import format_plaintext_for_html
 from misago.users.models import Rank
 
 
+__all__ = ['RankSerializer']
+
+
 class RankSerializer(serializers.ModelSerializer):
     description = serializers.SerializerMethodField()
+    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Rank
@@ -18,6 +22,7 @@ class RankSerializer(serializers.ModelSerializer):
             'css_class',
             'is_default',
             'is_tab',
+            'url',
         ]
 
     def get_description(self, obj):
@@ -25,3 +30,6 @@ class RankSerializer(serializers.ModelSerializer):
             return format_plaintext_for_html(obj.description)
         else:
             return ''
+
+    def get_url(self, obj):
+        return obj.get_absolute_url()

+ 0 - 118
misago/users/serializers/register.py

@@ -1,118 +0,0 @@
-from rest_framework import serializers
-
-from django.contrib.auth import get_user_model
-from django.contrib.auth.password_validation import validate_password
-from django.core.exceptions import ValidationError
-from django.utils import six
-from django.utils.translation import ugettext as _
-
-from misago.users import captcha, validators
-from misago.users.bans import get_email_ban, get_username_ban
-
-
-UserModel = get_user_model()
-
-
-class BaseRegisterUserSerializer(serializers.Serializer):
-    username = serializers.CharField(
-        max_length=255,
-        validators=[validators.validate_username],
-    )
-    email = serializers.CharField(
-        max_length=255,
-        validators=[validators.validate_email],
-    )
-
-    def validate_username(self, data):
-        ban = get_username_ban(data, registration_only=True)
-        if ban:
-            if ban.user_message:
-                raise ValidationError(ban.user_message)
-            else:
-                raise ValidationError(_("This usernane is not allowed."))
-        return data
-
-    def validate_email(self, data):
-        ban = get_email_ban(data, registration_only=True)
-        if ban:
-            if ban.user_message:
-                raise ValidationError(ban.user_message)
-            else:
-                raise ValidationError(_("This e-mail address is not allowed."))
-        return data
-
-    def validate_added_errors(self):
-        if self._added_errors:
-            # fail registration with additional errors
-            raise serializers.ValidationError(self._added_errors)
-            
-    def validate(self, data):
-        self._added_errors = {}
-        return data
-
-    def add_error(self, field, error):
-        """
-        custom implementation for quasi add_error feature for custom validators
-        we are doing some hacky introspection here to deconstruct ValidationError
-        """
-        self._added_errors.setdefault(field, [])
-
-        if isinstance(error, ValidationError):
-            self._added_errors[field].extend(list(error))
-        elif isinstance(error, serializers.ValidationError):
-            details = [e['message'] for e in error.get_full_details()]
-            self._added_errors[field].extend(details)
-        else:
-            self._added_errors[field].append(six.text_type(error))
-
-
-class SocialRegisterUserSerializer(BaseRegisterUserSerializer):
-    def validate(self, data):
-        data = super(SocialRegisterUserSerializer, self).validate(data)
-
-        request = self.context['request']
-
-        validators.validate_new_registration(request, data, self.add_error)
-
-        self.validate_added_errors()
-
-        return data
-
-
-class RegisterUserSerializer(BaseRegisterUserSerializer):
-    password = serializers.CharField(
-        max_length=255,
-        trim_whitespace=False,
-    )
-
-    def validate(self, data):
-        data = super(RegisterUserSerializer, self).validate(data)
-
-        request = self.context['request']
-
-        try:
-            self.full_clean_password(data)
-        except ValidationError as e:
-            self.add_error('password', e)
-
-        validators.validate_new_registration(request, data, self.add_error)
-
-        self.validate_added_errors()
-
-        # run test for captcha
-        try:
-            captcha.test_request(self.context['request'])
-        except ValidationError as e:
-            raise serializers.ValidationError({'captcha': e.message})
-
-        return data
-
-    def full_clean_password(self, data):
-        if data.get('password'):
-            validate_password(
-                data['password'],
-                user=UserModel(
-                    username=data.get('username'),
-                    email=data.get('email'),
-                ),
-            )

+ 94 - 1
misago/users/serializers/user.py

@@ -3,13 +3,15 @@ from rest_framework import serializers
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 
-from misago.api.serializers import MutableFields
+from misago.core.serializers import MutableFields
 
 from . import RankSerializer
 
 
 UserModel = get_user_model()
 
+__all__ = ['StatusSerializer', 'UserSerializer', 'UserCardSerializer']
+
 
 class StatusSerializer(serializers.Serializer):
     is_offline = serializers.BooleanField()
@@ -28,12 +30,16 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     rank = RankSerializer(many=False, read_only=True)
     signature = serializers.SerializerMethodField()
 
+    acl = serializers.SerializerMethodField()
     is_followed = serializers.SerializerMethodField()
     is_blocked = serializers.SerializerMethodField()
     meta = serializers.SerializerMethodField()
     real_name = serializers.SerializerMethodField()
     status = serializers.SerializerMethodField()
 
+    api = serializers.SerializerMethodField()
+    url = serializers.SerializerMethodField()
+
     class Meta:
         model = UserModel
         fields = [
@@ -52,14 +58,21 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
             'following',
             'threads',
             'posts',
+            'acl',
             'is_followed',
             'is_blocked',
 
             'meta',
             'real_name',
             'status',
+
+            'api',
+            'url',
         ]
 
+    def get_acl(self, obj):
+        return obj.acl
+
     def get_email(self, obj):
         if (obj == self.context['user'] or self.context['user'].acl_cache['can_see_users_emails']):
             return obj.email
@@ -96,6 +109,85 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return None
 
+    def get_api(self, obj):
+        return {
+            'index': reverse(
+                'misago:api:user-detail',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'follow': reverse(
+                'misago:api:user-follow',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'ban': reverse(
+                'misago:api:user-ban',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'details': reverse(
+                'misago:api:user-details',
+                kwargs={
+                    'pk': obj.pk,
+                }
+            ),
+            'edit_details': reverse(
+                'misago:api:user-edit-details',
+                kwargs={
+                    'pk': obj.pk,
+                }
+            ),
+            'moderate_avatar': reverse(
+                'misago:api:user-moderate-avatar',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'moderate_username': reverse(
+                'misago:api:user-moderate-username',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'delete': reverse(
+                'misago:api:user-delete',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'followers': reverse(
+                'misago:api:user-followers',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'follows': reverse(
+                'misago:api:user-follows',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'threads': reverse(
+                'misago:api:user-threads',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+            'posts': reverse(
+                'misago:api:user-posts',
+                kwargs={
+                    'pk': obj.pk
+                }
+            ),
+        }
+
+    def get_url(self, obj):
+        return obj.get_absolute_url()
+
 
 UserCardSerializer = UserSerializer.subset_fields(
     'id',
@@ -109,4 +201,5 @@ UserCardSerializer = UserSerializer.subset_fields(
     'posts',
     'real_name',
     'status',
+    'url',
 )

+ 3 - 1
misago/users/serializers/usernamechange.py

@@ -5,7 +5,9 @@ from misago.users.models import UsernameChange
 from .user import UserSerializer as BaseUserSerializer
 
 
-UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'slug', 'avatars')
+__all__ = ['UsernameChangeSerializer']
+
+UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'avatars', 'url')
 
 
 class UsernameChangeSerializer(serializers.ModelSerializer):

+ 87 - 1
misago/users/signals.py

@@ -1,11 +1,97 @@
+from collections import OrderedDict
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.db.models import Q
 from django.dispatch import Signal, receiver
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+
+from misago.conf import settings
+from misago.core.pgutils import chunk_queryset
+
+from .models import AuditTrail
+from .profilefields import profilefields
 
 
-anonymize_user_content = Signal()
+UserModel = get_user_model()
+
+anonymize_user_data = Signal()
+archive_user_data = Signal()
 delete_user_content = Signal()
+remove_old_ips = Signal()
 username_changed = Signal()
 
 
+@receiver(archive_user_data)
+def archive_user_details(sender, archive=None, **kwargs):
+    archive.add_dict('details', OrderedDict([
+        (_('Username'), sender.username),
+        (_('E-mail'), sender.email),
+        (_('Joined on'), sender.joined_on),
+        (_('Joined from ip'), sender.joined_from_ip or 'unavailable'),
+    ]))
+
+
+@receiver(archive_user_data)
+def archive_user_profile_fields(sender, archive=None, **kwargs):
+    clean_profile_fields = OrderedDict()
+    for profile_fields_group in profilefields.get_fields_groups():
+        for profile_field in profile_fields_group['fields']:
+            if sender.profile_fields.get(profile_field.fieldname):
+                field_value = sender.profile_fields[profile_field.fieldname]
+                clean_profile_fields[str(profile_field.label)] = field_value
+                
+    if clean_profile_fields:
+        archive.add_dict('profile_fields', clean_profile_fields)
+
+
+@receiver(archive_user_data)
+def archive_user_avatar(sender, archive=None, **kwargs):
+    archive.add_model_file(sender.avatar_tmp, directory='avatar', prefix='tmp')
+    archive.add_model_file(sender.avatar_src, directory='avatar', prefix='src')
+    for avatar in sender.avatar_set.iterator():
+        archive.add_model_file(avatar.image, directory='avatar', prefix=avatar.size)
+
+
+@receiver(archive_user_data)
+def archive_user_audit_trail(sender, archive=None, **kwargs):
+    queryset = sender.audittrail_set.order_by('id')
+    for audit_trail in chunk_queryset(queryset):
+        item_name = audit_trail.created_on.strftime('%H%M%S-audit-trail')
+        archive.add_text(item_name, audit_trail.ip_address, date=audit_trail.created_on)
+
+
+@receiver(archive_user_data)
+def archive_user_name_history(sender, archive=None, **kwargs):
+    for name_change in sender.namechanges.order_by('id').iterator():
+        item_name = name_change.changed_on.strftime('%H%M%S-name-change')
+        archive.add_dict(
+            item_name,
+            OrderedDict([
+                (_("New username"), name_change.new_username),
+                (_("Old username"), name_change.old_username),
+            ]),
+            date=name_change.changed_on,
+        )
+
+
 @receiver(username_changed)
 def handle_name_change(sender, **kwargs):
     sender.user_renames.update(changed_by_username=sender.username)
+
+
+@receiver(remove_old_ips)
+def remove_old_registrations_ips(sender, **kwargs):
+    datetime_cutoff = timezone.now() - timedelta(days=settings.MISAGO_IP_STORE_TIME)
+    ip_is_too_new = Q(joined_on__gt=datetime_cutoff)
+    ip_is_already_removed = Q(joined_from_ip__isnull=True)
+    
+    queryset = UserModel.objects.exclude(ip_is_too_new | ip_is_already_removed)
+    queryset.update(joined_from_ip=None)
+
+
+@receiver(remove_old_ips)
+def remove_old_audit_trails(sender, **kwargs):
+    removal_cutoff = timezone.now() - timedelta(days=settings.MISAGO_IP_STORE_TIME)
+    AuditTrail.objects.filter(created_on__lte=removal_cutoff).delete()

+ 19 - 9
misago/users/social/pipeline.py

@@ -10,11 +10,14 @@ from social_core.pipeline.partial import partial
 
 from misago.conf import settings
 from misago.core.exceptions import SocialAuthFailed, SocialAuthBanned
+from misago.legal.models import Agreement
 
 from misago.users.bans import get_request_ip_ban, get_user_ban
-from misago.users.serializers.register import SocialRegisterUserSerializer
+from misago.users.forms.register import SocialAuthRegisterForm
 from misago.users.models import Ban
-from misago.users.registration import get_registration_result_json, send_welcome_email
+from misago.users.registration import (
+    get_registration_result_json, save_user_agreements, send_welcome_email
+)
 from misago.users.validators import (
     ValidationError, validate_new_registration, validate_email, validate_username)
 
@@ -156,6 +159,7 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
     new_user = UserModel.objects.create_user(
         username, 
         email, 
+        create_audit_trail=True,
         joined_from_ip=request.user_ip, 
         set_default_avatar=True,
         **activation_kwargs
@@ -181,12 +185,16 @@ def create_user_with_form(strategy, details, backend, user=None, *args, **kwargs
         except (TypeError, ValueError):
             request_data = request.POST.copy()
             
-        serializer = SocialRegisterUserSerializer(data=request_data, context={'request': request})
-        if not serializer.is_valid():
-            return JsonResponse(serializer.errors, status=400)
+        form = SocialAuthRegisterForm(
+            request_data,
+            request=request,    
+            agreements=Agreement.objects.get_agreements(),
+        )
+        
+        if not form.is_valid():
+            return JsonResponse(form.errors, status=400)
 
-        validated_data = serializer.validated_data
-        email_verified = validated_data['email'] == details.get('email')
+        email_verified = form.cleaned_data['email'] == details.get('email')
 
         activation_kwargs = {}
         if settings.account_activation == 'admin':
@@ -196,8 +204,9 @@ def create_user_with_form(strategy, details, backend, user=None, *args, **kwargs
 
         try:
             new_user = UserModel.objects.create_user(
-                validated_data['username'],
-                validated_data['email'],
+                form.cleaned_data['username'],
+                form.cleaned_data['email'],
+                create_audit_trail=True,
                 joined_from_ip=request.user_ip,
                 set_default_avatar=True,
                 **activation_kwargs
@@ -205,6 +214,7 @@ def create_user_with_form(strategy, details, backend, user=None, *args, **kwargs
         except IntegrityError:
             return JsonResponse({'__all__': _("Please try resubmitting the form.")}, status=400)
 
+        save_user_agreements(new_user, form)
         send_welcome_email(request, new_user)
 
         return {'user': new_user, 'is_new': True}

+ 187 - 0
misago/users/tests/test_audittrail.py

@@ -0,0 +1,187 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.utils import timezone
+
+from misago.users.audittrail import create_audit_trail, create_user_audit_trail
+from misago.users.models import AuditTrail
+from misago.users.signals import remove_old_ips
+from misago.users.testutils import UserTestCase
+
+
+UserModel = get_user_model()
+
+USER_IP = '13.41.51.41'
+
+
+class MockRequest(object):
+    user_ip = USER_IP
+
+    def __init__(self, user):
+        self.user = user
+
+
+class CreateAuditTrailTests(UserTestCase):
+    def setUp(self):
+        super(CreateAuditTrailTests, self).setUp()
+
+        self.obj = UserModel.objects.create_user('BobBoberson', 'bob@example.com')
+
+    def test_create_audit_require_model(self):
+        """create_audit_trail requires model instance"""
+        anonymous_user = self.get_anonymous_user()
+        request = MockRequest(anonymous_user)
+        with self.assertRaises(ValueError):
+            create_audit_trail(request, anonymous_user)
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_create_audit_trail_anonymous_user(self):
+        """create_audit_trail doesn't record anonymous users"""
+        user = self.get_anonymous_user()
+        request = MockRequest(user)
+        create_audit_trail(request, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_create_audit_trail(self):
+        """create_audit_trail creates new db record"""
+        user = self.get_authenticated_user()
+        request = MockRequest(user)
+        create_audit_trail(request, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        self.assertEqual(audit_trail.user, user)
+        self.assertEqual(audit_trail.ip_address, request.user_ip)
+        self.assertEqual(audit_trail.content_object, self.obj)
+
+    def test_delete_user_remove_audit_trail(self):
+        """audit trail is deleted together with user it belongs to"""
+        user = self.get_authenticated_user()
+        request = MockRequest(user)
+        create_audit_trail(request, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        user.delete()
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_delete_obj_keep_audit_trail(self):
+        """audit trail is kept after with obj it points at is deleted"""
+        user = self.get_authenticated_user()
+        request = MockRequest(user)
+        create_audit_trail(request, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        self.obj.delete()
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        self.assertEqual(audit_trail.user, user)
+        self.assertEqual(audit_trail.ip_address, request.user_ip)
+        self.assertIsNone(audit_trail.content_object)
+
+    def test_delete_audit_trail(self):
+        """audit trail deletion leaves other data untouched"""
+        user = self.get_authenticated_user()
+        request = MockRequest(user)
+        create_audit_trail(request, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        audit_trail.delete()
+        
+        UserModel.objects.get(id=user.id)
+        UserModel.objects.get(id=self.obj.id)
+
+
+class CreateUserAuditTrailTests(UserTestCase):
+    def setUp(self):
+        super(CreateUserAuditTrailTests, self).setUp()
+
+        self.obj = UserModel.objects.create_user('BobBoberson', 'bob@example.com')
+
+    def test_create_user_audit_require_model(self):
+        """create_user_audit_trail requires model instance"""
+        anonymous_user = self.get_anonymous_user()
+        with self.assertRaises(ValueError):
+            create_user_audit_trail(anonymous_user, USER_IP, anonymous_user)
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_create_user_audit_trail_anonymous_user(self):
+        """create_user_audit_trail doesn't record anonymous users"""
+        user = self.get_anonymous_user()
+        create_user_audit_trail(user, USER_IP, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_create_user_audit_trail(self):
+        """create_user_audit_trail creates new db record"""
+        user = self.get_authenticated_user()
+        create_user_audit_trail(user, USER_IP, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        self.assertEqual(audit_trail.user, user)
+        self.assertEqual(audit_trail.ip_address, USER_IP)
+        self.assertEqual(audit_trail.content_object, self.obj)
+
+    def test_delete_user_remove_audit_trail(self):
+        """audit trail is deleted together with user it belongs to"""
+        user = self.get_authenticated_user()
+        create_user_audit_trail(user, USER_IP, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        user.delete()
+        self.assertEqual(AuditTrail.objects.count(), 0)
+
+    def test_delete_obj_keep_audit_trail(self):
+        """audit trail is kept after with obj it points at is deleted"""
+        user = self.get_authenticated_user()
+        create_user_audit_trail(user, USER_IP, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        self.obj.delete()
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        self.assertEqual(audit_trail.user, user)
+        self.assertEqual(audit_trail.ip_address, USER_IP)
+        self.assertIsNone(audit_trail.content_object)
+
+    def test_delete_audit_trail(self):
+        """audit trail deletion leaves other data untouched"""
+        user = self.get_authenticated_user()
+        create_user_audit_trail(user, USER_IP, self.obj)
+        self.assertEqual(AuditTrail.objects.count(), 1)
+
+        audit_trail = user.audittrail_set.all()[0]
+        audit_trail.delete()
+        
+        UserModel.objects.get(id=user.id)
+        UserModel.objects.get(id=self.obj.id)
+
+
+class RemoveOldAuditTrailsTest(UserTestCase):
+    def setUp(self):
+        super(RemoveOldAuditTrailsTest, self).setUp()
+
+        self.obj = UserModel.objects.create_user('BobBoberson', 'bob@example.com')
+        
+    def test_recent_audit_trail_is_kept(self):
+        """remove_old_ips keeps recent audit trails"""
+        user = self.get_authenticated_user()
+        audit_trail = create_user_audit_trail(user, USER_IP, self.obj)
+
+        remove_old_ips.send(None)
+
+        self.assertEqual(user.audittrail_set.count(), 1)
+
+    def test_old_audit_trail_is_removed(self):
+        """remove_old_ips removes old audit trails"""
+        user = self.get_authenticated_user()
+        audit_trail = create_user_audit_trail(user, USER_IP, self.obj)
+
+        audit_trail.created_on = timezone.now() - timedelta(days=50)
+        audit_trail.save()
+
+        remove_old_ips.send(None)
+
+        self.assertEqual(user.audittrail_set.count(), 0)

+ 94 - 247
misago/users/tests/test_auth_api.py

@@ -18,16 +18,14 @@ class GatewayTests(TestCase):
                 'password': 'nope',
             }
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Login or password is incorrect."],
-        })
-        
+
+        self.assertContains(response, "Login or password is incorrect.", status_code=400)
+
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
     def test_login(self):
         """api signs user in"""
@@ -46,9 +44,9 @@ class GatewayTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], user.id)
-        self.assertEqual(response_json['username'], user.username)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], user.id)
+        self.assertEqual(user_json['username'], user.username)
 
     def test_login_whitespaces_password(self):
         """api signs user in with password left untouched"""
@@ -61,10 +59,8 @@ class GatewayTests(TestCase):
                 'password': 'Pass.123',
             },
         )
+
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Login or password is incorrect."],
-        })
 
         response = self.client.post(
             '/api/auth/',
@@ -79,32 +75,14 @@ class GatewayTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], user.id)
-        self.assertEqual(response_json['username'], user.username)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], user.id)
+        self.assertEqual(user_json['username'], user.username)
 
-    def test_submit_no_data(self):
+    def test_submit_empty(self):
         """login api errors for no body"""
         response = self.client.post('/api/auth/')
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ['This field is required.'],
-            'password': ['This field is required.'],
-        })
-
-    def test_submit_empty(self):
-        """login api errors for empty fields"""
-        response = self.client.post('/api/auth/', data={
-            'username': '',
-            'password': '',
-        })
-
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ['This field may not be blank.'],
-            'password': ['This field may not be blank.'],
-        })
+        self.assertContains(response, 'empty_data', status_code=400)
 
     def test_submit_invalid(self):
         """login api errors for invalid data"""
@@ -113,10 +91,7 @@ class GatewayTests(TestCase):
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_login_not_usable_password(self):
         """login api fails to sign user with not-usable password in"""
@@ -131,7 +106,8 @@ class GatewayTests(TestCase):
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
-            'non_field_errors': ["Login or password is incorrect."],
+            'code': 'invalid_login',
+            'detail': 'Login or password is incorrect.',
         })
 
     def test_login_banned(self):
@@ -151,20 +127,20 @@ class GatewayTests(TestCase):
                 'password': 'Pass.123',
             },
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': {
-                'html': '<p>%s</p>' % ban.user_message,
-                'plain': ban.user_message,
-            },
-            'expires_on': None,
-        })
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['code'], 'banned')
+        self.assertEqual(response_json['detail']['message']['plain'], ban.user_message)
+        self.assertEqual(
+            response_json['detail']['message']['html'], '<p>%s</p>' % ban.user_message
+        )
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
     def test_login_banned_staff(self):
         """login api signs banned staff member in"""
@@ -191,9 +167,9 @@ class GatewayTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], user.id)
-        self.assertEqual(response_json['username'], user.username)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], user.id)
+        self.assertEqual(user_json['username'], user.username)
 
     def test_login_ban_registration_only(self):
         """login api ignores registration-only bans"""
@@ -223,7 +199,7 @@ class GatewayTests(TestCase):
 
     def test_login_inactive_admin(self):
         """login api fails to sign admin-activated user in"""
-        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=2)
+        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
 
         response = self.client.post(
             '/api/auth/',
@@ -233,21 +209,19 @@ class GatewayTests(TestCase):
             },
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "Your account has to be activated by Administrator before you will be able to sign in.",
-            ],
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['code'], 'inactive_user')
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
     def test_login_inactive_user(self):
         """login api fails to sign user-activated user in"""
-        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
+        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=2)
 
         response = self.client.post(
             '/api/auth/',
@@ -257,17 +231,15 @@ class GatewayTests(TestCase):
             },
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "You have to activate your account before you will be able to sign in.",
-            ],
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['code'], 'inactive_admin')
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
     def test_login_disabled_user(self):
         """its impossible to sign in to disabled account"""
@@ -283,38 +255,20 @@ class GatewayTests(TestCase):
                 'password': 'Pass.123',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Login or password is incorrect."],
-        })
+        self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
 
-class UserRequirementsTests(TestCase):
+class UserCredentialsTests(TestCase):
     def test_edge_returns_response(self):
         """api edge has no showstoppers"""
-        response = self.client.get('/api/auth/requirements/')
+        response = self.client.get('/api/auth/criteria/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'username': {'max_length': 14, 'min_length': 3},
-            'password': [
-                {
-                    'name': 'UserAttributeSimilarityValidator',
-                    'user_attributes': ['username', 'email'],
-                },
-                {
-                    'name': 'MinimumLengthValidator',
-                    'min_length': 7,
-                },
-                {'name': 'CommonPasswordValidator'},
-                {'name': 'NumericPasswordValidator'},
-            ],
-        })
 
 
 class SendActivationAPITests(TestCase):
@@ -338,8 +292,8 @@ class SendActivationAPITests(TestCase):
         self.assertIn('Activate Bob', mail.outbox[0].subject)
 
     def test_submit_banned(self):
-        """request activation link api errors for banned users"""
-        ban = Ban.objects.create(
+        """request activation link api passes for banned users"""
+        Ban.objects.create(
             check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
@@ -351,16 +305,9 @@ class SendActivationAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': {
-                'html': '<p>%s</p>' % ban.user_message,
-                'plain': ban.user_message,
-            },
-            'expires_on': None,
-        })
+        self.assertEqual(response.status_code, 200)
 
-        self.assertTrue(not mail.outbox)
+        self.assertIn('Activate Bob', mail.outbox[0].subject)
 
     def test_submit_disabled(self):
         """request activation link api fails disabled users"""
@@ -373,22 +320,14 @@ class SendActivationAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        # fixme: don't leak out the info that email is invalid in auth forms
-        # instead, message that if email was valid you'll get an email
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["No user with this e-mail exists."],
-        })
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
     def test_submit_empty(self):
         """request activation link api errors for no body"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'email': ["This field is required."],
-        })
+        self.assertContains(response, 'empty_email', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -399,10 +338,7 @@ class SendActivationAPITests(TestCase):
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_submit_invalid_email(self):
         """request activation link api errors for invalid email"""
@@ -412,12 +348,7 @@ class SendActivationAPITests(TestCase):
                 'email': 'fake@mail.com',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        # fixme: don't leak out the info that email is invalid in auth forms
-        # instead, message that if email was valid you'll get an email
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["No user with this e-mail exists."],
-        })
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -432,10 +363,7 @@ class SendActivationAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Bob, your account is already active."],
-        })
+        self.assertContains(response, 'Bob, your account is already active.', status_code=400)
 
     def test_submit_inactive_user(self):
         """request activation link api errors for admin-activated users"""
@@ -448,10 +376,7 @@ class SendActivationAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Bob, only administrator may activate your account."],
-        })
+        self.assertContains(response, 'inactive_admin', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -464,10 +389,7 @@ class SendActivationAPITests(TestCase):
                 'email': self.user.email,
             }
         )
-        self.assertEqual(response.json(), {
-            'username': self.user.username,
-            'email': self.user.email,
-        })
+        self.assertEqual(response.status_code, 200)
 
         self.assertTrue(mail.outbox)
 
@@ -491,8 +413,8 @@ class SendPasswordFormAPITests(TestCase):
         self.assertIn('Change Bob password', mail.outbox[0].subject)
 
     def test_submit_banned(self):
-        """request change password form link api errors for banned users"""
-        ban = Ban.objects.create(
+        """request change password form link api sends reset link mail"""
+        Ban.objects.create(
             check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
@@ -504,16 +426,9 @@ class SendPasswordFormAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': {
-                'html': '<p>%s</p>' % ban.user_message,
-                'plain': ban.user_message,
-            },
-            'expires_on': None,
-        })
+        self.assertEqual(response.status_code, 200)
 
-        self.assertTrue(not mail.outbox)
+        self.assertIn('Change Bob password', mail.outbox[0].subject)
 
     def test_submit_disabled(self):
         """request change password form api fails disabled users"""
@@ -526,20 +441,14 @@ class SendPasswordFormAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["No user with this e-mail exists."],
-        })
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
     def test_submit_empty(self):
         """request change password form link api errors for no body"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'email': ["This field is required."],
-        })
+        self.assertContains(response, 'empty_email', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -551,10 +460,7 @@ class SendPasswordFormAPITests(TestCase):
                 'email': 'fake@mail.com',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["No user with this e-mail exists."],
-        })
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -565,10 +471,7 @@ class SendPasswordFormAPITests(TestCase):
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
+        self.assertContains(response, "Invalid data.", status_code=400)
 
     def test_submit_inactive_user(self):
         """request change password form link api errors for inactive users"""
@@ -581,14 +484,7 @@ class SendPasswordFormAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "You have to activate your account before you will "
-                "be able to request new password.",
-            ],
-        })
-        self.assertTrue(not mail.outbox)
+        self.assertContains(response, 'inactive_user', status_code=400)
 
         self.user.requires_activation = 2
         self.user.save()
@@ -599,28 +495,23 @@ class SendPasswordFormAPITests(TestCase):
                 'email': self.user.email,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "Administrator has to activate your account before you "
-                "will be able to request new password.",
-            ],
-        })
+        self.assertContains(response, 'inactive_admin', status_code=400)
+
         self.assertTrue(not mail.outbox)
 
 
 class ChangePasswordAPITests(TestCase):
     def setUp(self):
         self.user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.link = '/api/auth/change-password/%s/'
+
+        self.link = '/api/auth/change-password/%s/%s/'
 
     def test_submit_valid(self):
         """submit change password form api changes password"""
         response = self.client.post(
-            self.link % self.user.pk,
+            self.link % (self.user.pk, make_password_change_token(self.user)),
             data={
                 'password': 'n3wp4ss!',
-                'token': make_password_change_token(self.user),
             },
         )
         self.assertEqual(response.status_code, 200)
@@ -631,10 +522,9 @@ class ChangePasswordAPITests(TestCase):
     def test_submit_with_whitespaces(self):
         """submit change password form api changes password with whitespaces"""
         response = self.client.post(
-            self.link % self.user.pk,
+            self.link % (self.user.pk, make_password_change_token(self.user)),
             data={
                 'password': ' n3wp4ss! ',
-                'token': make_password_change_token(self.user),
             },
         )
         self.assertEqual(response.status_code, 200)
@@ -645,52 +535,30 @@ class ChangePasswordAPITests(TestCase):
     def test_submit_invalid_data(self):
         """login api errors for invalid data"""
         response = self.client.post(
-            self.link % self.user.pk,
+            self.link % (self.user.pk, make_password_change_token(self.user)),
             'false',
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
+        self.assertContains(response, "Invalid data.", status_code=400)
 
-    def test_invalid_token(self):
+    def test_invalid_token_link(self):
         """api errors on invalid user id link"""
-        response = self.client.post(
-            self.link % self.user.pk,
-            data={
-                'password': 'n3wp4ss!',
-                'token': 'invalid!',
-            },
-        )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'token': ["Form link is invalid or expired. Please try again."],
-        })
+        response = self.client.post(self.link % (self.user.pk, 'asda7ad89sa7d9s789as'))
+
+        self.assertContains(response, "Form link is invalid.", status_code=400)
 
     def test_banned_user_link(self):
         """request errors because user is banned"""
-        ban = Ban.objects.create(
+        Ban.objects.create(
             check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
         )
 
         response = self.client.post(
-            self.link % self.user.pk,
-            data={
-                'password': 'n3wp4ss!',
-                'token': make_password_change_token(self.user),
-            },
+            self.link % (self.user.pk, make_password_change_token(self.user))
         )
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': {
-                'html': '<p>%s</p>' % ban.user_message,
-                'plain': ban.user_message,
-            },
-            'expires_on': None,
-        })
+        self.assertContains(response, "Your link has expired.", status_code=400)
 
     def test_inactive_user(self):
         """change password api errors for inactive users"""
@@ -698,52 +566,31 @@ class ChangePasswordAPITests(TestCase):
         self.user.save()
 
         response = self.client.post(
-            self.link % self.user.pk,
-            data={
-                'password': 'n3wp4ss!',
-                'token': make_password_change_token(self.user),
-            },
+            self.link % (self.user.pk, make_password_change_token(self.user))
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "You have to activate your account before you will "
-                "be able to change your password.",
-            ],
-        })
+        self.assertContains(response, "Your link has expired.", status_code=400)
 
         self.user.requires_activation = 2
         self.user.save()
 
         response = self.client.post(
-            self.link % self.user.pk,
-            data={
-                'password': 'n3wp4ss!',
-                'token': make_password_change_token(self.user),
-            },
+            self.link % (self.user.pk, make_password_change_token(self.user))
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': [
-                "Administrator has to activate your account before you "
-                "will be able to change your password.",
-            ],
-        })
+        self.assertContains(response, "Your link has expired.", status_code=400)
 
     def test_disabled_user(self):
         """change password api errors for disabled users"""
         self.user.is_active = False
         self.user.save()
 
-        response = self.client.post(self.link % self.user.pk)
-        self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
+        self.assertContains(response, "Form link is invalid.", status_code=400)
 
     def test_submit_empty(self):
         """change password api errors for empty body"""
-        response = self.client.post(self.link % self.user.pk)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'password': ["This field is required."],
-            'token': ["This field is required."],
-        })
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
+        self.assertContains(response, "This password is too shor", status_code=400)

+ 4 - 4
misago/users/tests/test_auth_views.py

@@ -78,8 +78,8 @@ class AuthViewsTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
         response = self.client.post(reverse('misago:logout'))
         self.assertEqual(response.status_code, 302)
@@ -87,5 +87,5 @@ class AuthViewsTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])

+ 0 - 10
misago/users/tests/test_bio_profilefield.py

@@ -131,11 +131,6 @@ class BioProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
@@ -166,11 +161,6 @@ class BioProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]

+ 324 - 0
misago/users/tests/test_datadownloads.py

@@ -0,0 +1,324 @@
+import os
+
+from django.core.files import File
+
+from misago.categories.models import Category
+from misago.threads.models import Attachment, AttachmentType
+from misago.threads.testutils import post_thread, post_poll
+from misago.users.audittrail import create_user_audit_trail
+from misago.users.datadownloads import (
+    expire_user_data_download, prepare_user_data_download, request_user_data_download,
+    user_has_data_download_request
+)
+from misago.users.models import DataDownload
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
+TEST_FILE_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
+
+
+class ExpireUserDataDownloadTests(AuthenticatedUserTestCase):
+    def test_util_marks_download_as_expired(self):
+        """expire_user_data_download changed data download status to expired"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+
+        with open(TEST_FILE_PATH, 'rb') as download_file:
+            data_download.file = File(download_file)
+            data_download.save()
+
+        expire_user_data_download(data_download)
+
+        self.assertEqual(data_download.status, DataDownload.STATUS_EXPIRED)
+
+    def test_util_deletes_file(self):
+        """expire_user_data_download deleted file associated with download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+
+        with open(TEST_FILE_PATH, 'rb') as download_file:
+            data_download.file = File(download_file)
+            data_download.save()
+
+        download_file_path = data_download.file.path
+
+        expire_user_data_download(data_download)
+
+        self.assertFalse(data_download.file)
+        self.assertFalse(os.path.isdir(download_file_path))
+
+    def test_util_expires_download_without_file(self):
+        """expire_user_data_download handles missing download file"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+
+        expire_user_data_download(data_download)
+
+        self.assertEqual(data_download.status, DataDownload.STATUS_EXPIRED)
+
+
+class PrepareUserDataDownload(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PrepareUserDataDownload, self).setUp()
+        self.download = request_user_data_download(self.user)
+
+    def assert_download_is_valid(self):
+        result = prepare_user_data_download(self.download)
+        self.assertTrue(result)
+
+        self.download.refresh_from_db()
+        self.assertTrue(self.download.file)
+
+    def test_prepare_basic_download(self):
+        """function creates data download for basic user account"""
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_profle_fields(self):
+        """function creates data download for user with profile fields"""
+        self.user.profile_fields = {'real_name': "Bob Boberthon!"}
+        self.user.save()
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_tmp_avatar(self):
+        """function creates data download for user with tmp avatar"""
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.avatar_tmp = File(test_file)
+            self.user.save()
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_src_avatar(self):
+        """function creates data download for user with src avatar"""
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.avatar_src = File(test_file)
+            self.user.save()
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_avatar_set(self):
+        """function creates data download for user with avatar set"""
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.avatar_set.create(size=100, image=File(test_file))
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_file_attachment(self):
+        """function creates data download for user with file attachment"""
+        filetype = AttachmentType.objects.create(
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
+        )
+
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.attachment_set.create(
+                secret='test',
+                filetype=filetype,
+                uploader_name=self.user.username,
+                uploader_slug=self.user.slug,
+                filename='test.png',
+                size=1000,
+                file=File(test_file),
+            )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_image_attachment(self):
+        """function creates data download for user with image attachment"""
+        filetype = AttachmentType.objects.create(
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
+        )
+
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.attachment_set.create(
+                secret='test',
+                filetype=filetype,
+                uploader_name=self.user.username,
+                uploader_slug=self.user.slug,
+                filename='test.png',
+                size=1000,
+                image=File(test_file),
+            )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_thumbnail_attachment(self):
+        """function creates data download for user with thumbnail attachment"""
+        filetype = AttachmentType.objects.create(
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
+        )
+
+        with open(TEST_FILE_PATH, 'rb') as test_file:
+            self.user.attachment_set.create(
+                secret='test',
+                filetype=filetype,
+                uploader_name=self.user.username,
+                uploader_slug=self.user.slug,
+                filename='test.png',
+                size=1000,
+                thumbnail=File(test_file),
+            )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_self_username_change(self):
+        """function creates data download for user that changed their username"""
+        self.user.record_name_change(self.user, 'aerith', 'alice')
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_username_changed_by_staff(self):
+        """function creates data download for user with username changed by staff"""
+        staff_user = self.get_superuser()
+        self.user.record_name_change(staff_user, 'aerith', 'alice')
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_username_changed_by_deleted_user(self):
+        """function creates data download for user with username changed by deleted user"""
+        self.user.record_name_change(self.user, 'aerith', 'alice')
+        self.user.namechanges.update(changed_by=None)
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_audit_trail(self):
+        """function creates data download for user with audit trail"""
+        create_user_audit_trail(self.user, '127.0.0.1', self.user)
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_post(self):
+        """function creates data download for user with post"""
+        category = Category.objects.get(slug='first-category')
+        post_thread(category, poster=self.user)
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_owm_post_edit(self):
+        """function creates data download for user with own post edit"""
+        category = Category.objects.get(slug='first-category')
+        thread = post_thread(category, poster=self.user)
+        post = thread.first_post
+
+        post.edits_record.create(
+            category=category,
+            thread=thread,
+            editor=self.user,
+            editor_name=self.user.username,
+            editor_slug=self.user.slug,
+            edited_from="edited from",
+            edited_to="edited to",
+        )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_other_users_post_edit(self):
+        """function creates data download for user with other user's post edit"""
+        category = Category.objects.get(slug='first-category')
+        thread = post_thread(category)
+        post = thread.first_post
+
+        post.edits_record.create(
+            category=category,
+            thread=thread,
+            editor=self.user,
+            editor_name=self.user.username,
+            editor_slug=self.user.slug,
+            edited_from="edited from",
+            edited_to="edited to",
+        )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_own_post_edit_by_staff(self):
+        """function creates data download for user with post edited by staff"""
+        category = Category.objects.get(slug='first-category')
+        thread = post_thread(category, poster=self.user)
+        post = thread.first_post
+
+        staff_user = self.get_superuser()
+
+        post.edits_record.create(
+            category=category,
+            thread=thread,
+            editor=staff_user,
+            editor_name=staff_user.username,
+            editor_slug=staff_user.slug,
+            edited_from="edited from",
+            edited_to="edited to",
+        )
+
+        self.assert_download_is_valid()
+
+    def test_prepare_download_with_poll(self):
+        """function creates data download for user with poll"""
+        category = Category.objects.get(slug='first-category')
+        thread = post_thread(category, poster=self.user)
+        post_poll(thread, self.user)
+
+        self.assert_download_is_valid()
+
+
+class RequestUserDataDownloadTests(AuthenticatedUserTestCase):
+    def test_util_creates_data_download_for_user_with_them_as_requester(self):
+        """request_user_data_download created valid data download for user"""
+        data_download = request_user_data_download(self.user)
+
+        self.assertEqual(data_download.user, self.user)
+        self.assertEqual(data_download.requester, self.user)
+        self.assertEqual(data_download.requester_name, self.user.username)
+        self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
+
+    def test_util_creates_data_download_for_user_explicit_requester(self):
+        """request_user_data_download created valid data download for user with other requester"""
+        requester = self.get_superuser()
+        data_download = request_user_data_download(self.user, requester)
+
+        self.assertEqual(data_download.user, self.user)
+        self.assertEqual(data_download.requester, requester)
+        self.assertEqual(data_download.requester_name, requester.username)
+        self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
+
+
+class UserHasRequestedDataDownloadTests(AuthenticatedUserTestCase):
+    def test_util_returns_false_for_no_download(self):
+        """user_has_data_download_request returns false if user has no requests in progress"""
+        self.assertFalse(user_has_data_download_request(self.user))
+
+    def test_util_returns_false_for_ready_download(self):
+        """user_has_data_download_request returns false if user has ready download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+        data_download.save()
+
+        self.assertFalse(user_has_data_download_request(self.user))
+
+    def test_util_returns_false_for_expired_download(self):
+        """user_has_data_download_request returns false if user has expired download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_EXPIRED
+        data_download.save()
+        
+        self.assertFalse(user_has_data_download_request(self.user))
+
+    def test_util_returns_true_for_pending_download(self):
+        """user_has_data_download_request returns true if user has pending download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_PENDING
+        data_download.save()
+        
+        self.assertTrue(user_has_data_download_request(self.user))
+
+    def test_util_returns_true_for_processing_download(self):
+        """user_has_data_download_request returns true if user has processing download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_PROCESSING
+        data_download.save()
+        
+        self.assertTrue(user_has_data_download_request(self.user))

+ 245 - 0
misago/users/tests/test_datadownloads_dataarchive.py

@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+import io  # fixme: remove explicit io imports after going py3k-only
+import os
+from collections import OrderedDict
+
+from django.core.files import File
+from django.test import TestCase
+from django.utils import timezone
+
+from misago.conf import settings
+from misago.users.datadownloads.dataarchive import FILENAME_MAX_LEN, DataArchive, trim_long_filename
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+DATA_DOWNLOADS_WORKING_DIR = settings.MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR
+TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
+TEST_AVATAR_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
+
+
+class DataArchiveTests(AuthenticatedUserTestCase):
+    def test_enter_without_dirs(self):
+        """data archive doesn't touch filesystem on init"""
+        archive = DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR)
+
+        self.assertEqual(archive.user, self.user)
+        self.assertEqual(archive.working_dir_path, DATA_DOWNLOADS_WORKING_DIR)
+
+        self.assertIsNone(archive.tmp_dir_path)
+        self.assertIsNone(archive.data_dir_path)
+
+    def test_context_life_cycle(self):
+        """object creates valid tmp directory on enter and cleans on exit"""
+        tmp_dir_path = None
+        data_dir_path = None
+
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            self.assertTrue(os.path.exists(archive.tmp_dir_path))
+            self.assertTrue(os.path.exists(archive.data_dir_path))
+
+            working_dir = str(DATA_DOWNLOADS_WORKING_DIR)
+            tmp_dir_path = str(archive.tmp_dir_path)
+            data_dir_path = str(archive.data_dir_path)
+            
+            self.assertTrue(tmp_dir_path.startswith(working_dir))
+            self.assertTrue(data_dir_path.startswith(working_dir))
+
+            self.assertTrue(data_dir_path.startswith(working_dir))
+            self.assertTrue(data_dir_path.startswith(tmp_dir_path))
+
+        self.assertIsNone(archive.tmp_dir_path)
+        self.assertIsNone(archive.data_dir_path)
+
+        self.assertFalse(os.path.exists(tmp_dir_path))
+        self.assertFalse(os.path.exists(data_dir_path))
+
+    def test_add_text_str(self):
+        """add_dict method creates text file with string"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            data_to_write = u"Hello, łorld!"
+            file_path = archive.add_text('testfile', data_to_write)
+            self.assertTrue(os.path.isfile(file_path))
+
+            valid_output_path = os.path.join(archive.data_dir_path, 'testfile.txt')
+            self.assertEqual(file_path, valid_output_path)
+
+            with io.open(file_path, 'r', encoding="utf-8") as fp:
+                saved_data = fp.read().strip()
+                self.assertEqual(saved_data, data_to_write)
+
+    def test_add_text_int(self):
+        """add_dict method creates text file with int"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            data_to_write = 1234
+            file_path = archive.add_text('testfile', data_to_write)
+            self.assertTrue(os.path.isfile(file_path))
+
+            valid_output_path = os.path.join(archive.data_dir_path, 'testfile.txt')
+            self.assertEqual(file_path, valid_output_path)
+
+            with io.open(file_path, 'r', encoding="utf-8") as fp:
+                saved_data = fp.read().strip()
+                self.assertEqual(saved_data, str(data_to_write))
+
+    def test_add_dict(self):
+        """add_dict method creates text file from dict"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            data_to_write = {'first': u"łorld!", 'second': u"łup!"}
+            file_path = archive.add_dict('testfile', data_to_write)
+            self.assertTrue(os.path.isfile(file_path))
+
+            valid_output_path = os.path.join(archive.data_dir_path, 'testfile.txt')
+            self.assertEqual(file_path, valid_output_path)
+
+            with io.open(file_path, 'r', encoding="utf-8") as fp:
+                saved_data = fp.read().strip()
+                # order of dict items in py<3.6 is non-deterministic
+                # making testing for exact match a mistake
+                self.assertIn(u"first: łorld!", saved_data)
+                self.assertIn(u"second: łup!", saved_data)
+
+    def test_add_dict_ordered(self):
+        """add_dict method creates text file form ordered dict"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            data_to_write = OrderedDict((('first', u"łorld!"), ('second', u"łup!")))
+            file_path = archive.add_dict('testfile', data_to_write)
+            self.assertTrue(os.path.isfile(file_path))
+
+            valid_output_path = os.path.join(archive.data_dir_path, 'testfile.txt')
+            self.assertEqual(file_path, valid_output_path)
+
+            with io.open(file_path, 'r', encoding="utf-8") as fp:
+                saved_data = fp.read().strip()
+                self.assertEqual(saved_data, u"first: łorld!\nsecond: łup!")
+
+    def test_add_model_file(self):
+        """add_model_file method adds model file"""
+        with io.open(TEST_AVATAR_PATH, 'rb') as avatar:
+            self.user.avatar_tmp = File(avatar)
+            self.user.save()
+
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            file_path = archive.add_model_file(self.user.avatar_tmp)
+
+            self.assertTrue(os.path.isfile(file_path))
+    
+            data_dir_path = str(archive.data_dir_path)
+            self.assertTrue(str(file_path).startswith(data_dir_path))
+
+    def test_add_model_file_empty(self):
+        """add_model_file method is noop if model field is empty"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            file_path = archive.add_model_file(self.user.avatar_tmp)
+
+            self.assertIsNone(file_path)
+            self.assertFalse(os.listdir(archive.data_dir_path))
+
+    def test_add_model_file_prefixed(self):
+        """add_model_file method adds model file with prefix"""
+        with io.open(TEST_AVATAR_PATH, 'rb') as avatar:
+            self.user.avatar_tmp = File(avatar)
+            self.user.save()
+
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            file_path = archive.add_model_file(self.user.avatar_tmp, prefix="prefix")
+
+            self.assertTrue(os.path.isfile(file_path))
+    
+            data_dir_path = str(archive.data_dir_path)
+            self.assertTrue(str(file_path).startswith(data_dir_path))
+            
+            filename = os.path.basename(self.user.avatar_tmp.name)
+            target_filename = 'prefix-{}'.format(filename)
+            self.assertTrue(str(file_path).endswith(target_filename))
+
+    def test_make_final_path_no_kwargs(self):
+        """make_final_path returns data_dir_path if no kwargs are set"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            final_path = archive.make_final_path()
+            self.assertEqual(final_path, archive.data_dir_path)
+
+    def test_make_final_path_directory(self):
+        """make_final_path returns path including directory name"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            final_path = archive.make_final_path(directory='test-directory')
+            valid_path = os.path.join(archive.data_dir_path, 'test-directory')
+            self.assertEqual(final_path, valid_path)
+
+    def test_make_final_path_date(self):
+        """make_final_path returns path including date segments"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            now = timezone.now().date()
+            final_path = archive.make_final_path(date=now)
+            
+            valid_path = os.path.join(
+                archive.data_dir_path,
+                now.strftime('%Y'),
+                now.strftime('%m'),
+                now.strftime('%d')
+            )
+
+            self.assertEqual(final_path, valid_path)
+
+    def test_make_final_path_datetime(self):
+        """make_final_path returns path including date segments"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            now = timezone.now()
+            final_path = archive.make_final_path(date=now)
+            
+            valid_path = os.path.join(
+                archive.data_dir_path,
+                now.strftime('%Y'),
+                now.strftime('%m'),
+                now.strftime('%d')
+            )
+
+            self.assertEqual(final_path, valid_path)
+
+    def test_make_final_path_both_kwargs(self):
+        """make_final_path raises value error if both date and directory are set"""
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            expected_message = "date and directory arguments are mutually exclusive"
+            with self.assertRaisesMessage(ValueError, expected_message):
+                archive.make_final_path(date=timezone.now(), directory='test')
+
+    def test_get_file(self):
+        """get_file returns django file"""
+        django_file = None
+        
+        with io.open(TEST_AVATAR_PATH, 'rb') as avatar:
+            self.user.avatar_tmp = File(avatar)
+            self.user.save()
+
+        with DataArchive(self.user, DATA_DOWNLOADS_WORKING_DIR) as archive:
+            archive.add_model_file(self.user.avatar_tmp)
+
+            django_file = archive.get_file()
+            archive_path = archive.file_path
+
+            self.assertIsNotNone(archive_path)
+            self.assertEqual(django_file.name, archive.file_path)
+            self.assertFalse(django_file.closed)
+
+        self.assertIsNone(archive.file)
+        self.assertIsNone(archive.file_path)
+        self.assertTrue(django_file.closed)
+
+
+class TrimLongFilenameTests(TestCase):
+    def test_trim_short_filename(self):
+        """trim_too_long_filename returns short filename as it is"""
+        filename = 'filename.jpg'
+        trimmed_filename = trim_long_filename(filename)
+        self.assertEqual(trimmed_filename, filename)
+
+    def test_trim_too_long_filename(self):
+        """trim_too_long_filename trims filename if its longer than allowed"""
+        filename = 'filename'
+        extension = '.jpg'
+        long_filename = '{}{}'.format(filename * 10, extension)
+
+        trimmed_filename = trim_long_filename(long_filename)
+        
+        self.assertEqual(len(trimmed_filename), FILENAME_MAX_LEN)
+        self.assertTrue(trimmed_filename.startswith(filename))
+        self.assertTrue(trimmed_filename.endswith(extension))

+ 133 - 0
misago/users/tests/test_datadownloadsadmin_views.py

@@ -0,0 +1,133 @@
+import os
+
+from django.contrib.auth import get_user_model
+from django.core.files import File
+from django.urls import reverse
+
+from misago.admin.testutils import AdminTestCase
+from misago.users.datadownloads import request_user_data_download
+from misago.users.models import DataDownload
+
+
+UserModel = get_user_model()
+
+TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
+TEST_FILE_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
+
+
+class DataDownloadAdminViewsTests(AdminTestCase):
+    def test_link_registered(self):
+        """admin nav contains data downloads link"""
+        response = self.client.get(reverse('misago:admin:users:accounts:index'))
+
+        response = self.client.get(response['location'])
+        self.assertContains(response, reverse('misago:admin:users:data-downloads:index'))
+
+    def test_list_view(self):
+        """data downloads list view returns 200"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:index'))
+        self.assertEqual(response.status_code, 302)
+
+        view_url = response['location']
+
+        response = self.client.get(view_url)
+        self.assertEqual(response.status_code, 200)
+
+        request_user_data_download(self.user)
+        response = self.client.get(view_url)
+        self.assertEqual(response.status_code, 200)
+
+    def test_expire_action(self):
+        """expire action marks data download as expired and deletes its file"""
+        data_download = request_user_data_download(self.user)
+
+        with open(TEST_FILE_PATH, 'rb') as upload:
+            data_download.file = File(upload)
+            data_download.save()
+
+        self.assertIsNotNone(data_download.file)
+        self.assertTrue(os.path.isfile(data_download.file.path))
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:index'),
+            data={
+                'action': 'expire',
+                'selected_items': [data_download.pk],
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        updated_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_download.status, DataDownload.STATUS_EXPIRED)
+        self.assertFalse(updated_download.file)
+
+        self.assertFalse(os.path.isfile(data_download.file.path))
+
+    def test_delete_action(self):
+        """dele action deletes data download together with its file"""
+        data_download = request_user_data_download(self.user)
+
+        with open(TEST_FILE_PATH, 'rb') as upload:
+            data_download.file = File(upload)
+            data_download.save()
+
+        self.assertIsNotNone(data_download.file)
+        self.assertTrue(os.path.isfile(data_download.file.path))
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:index'),
+            data={
+                'action': 'delete',
+                'selected_items': [data_download.pk],
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.count(), 0)
+        self.assertFalse(os.path.isfile(data_download.file.path))
+
+    def test_request_view(self):
+        """request data downloads view initializes new downloads"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:request'))
+        self.assertEqual(response.status_code, 200)
+
+        other_user = UserModel.objects.create_user('bob', 'bob@boberson.com')
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:request'),
+            data={
+                'user_identifiers': '\n'.join([
+                    self.user.username,
+                    other_user.email,
+                ]),
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.count(), 2)
+
+    def test_request_view_empty_data(self):
+        """request data downloads view handles empty data"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:request'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:request'),
+            data={'user_identifiers': ''},
+        )
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(DataDownload.objects.count(), 0)
+
+    def test_request_view_user_not_found(self):
+        """request data downloads view handles empty data"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:request'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:request'),
+            data={'user_identifiers': 'not@found.com'},
+        )
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(DataDownload.objects.count(), 0)

+ 109 - 0
misago/users/tests/test_deleteinactiveusers.py

@@ -0,0 +1,109 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.core.management import call_command
+from django.test import TestCase, override_settings
+from django.utils import timezone
+from django.utils.six import StringIO
+
+from misago.users.management.commands import deleteinactiveusers
+
+
+UserModel = get_user_model()
+
+
+class DeleteInactiveUsersTests(TestCase):
+    def setUp(self):
+        self.user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_delete_user_activation_user(self):
+        """deletes user that didn't activate their account within required time"""
+        self.user.joined_on = timezone.now() - timedelta(days=2)
+        self.user.requires_activation = UserModel.ACTIVATION_USER
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 1")
+
+        with self.assertRaises(UserModel.DoesNotExist):
+            UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_delete_user_activation_admin(self):
+        """deletes user that wasn't activated by admin within required time"""
+        self.user.joined_on = timezone.now() - timedelta(days=2)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 1")
+
+        with self.assertRaises(UserModel.DoesNotExist):
+            UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_new_user_activation_user(self):
+        """skips inactive user that is too new"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_USER
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_new_user_activation_admin(self):
+        """skips admin-activated user that is too new"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    def test_skip_active_user(self):
+        """skips active user"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Deleted users: 0")
+
+        UserModel.objects.get(pk=self.user.pk)
+
+    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=0)
+    def test_delete_inactive_is_disabled(self):
+        """skips active user"""
+        self.user.joined_on = timezone.now() - timedelta(days=1)
+        self.user.requires_activation = UserModel.ACTIVATION_ADMIN
+        self.user.save()
+
+        out = StringIO()
+        call_command(deleteinactiveusers.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(
+            command_output, "Automatic deletion of inactive users is currently disabled.")
+
+        UserModel.objects.get(pk=self.user.pk)

+ 107 - 0
misago/users/tests/test_expireuserdatadownloads.py

@@ -0,0 +1,107 @@
+import os
+from datetime import timedelta
+
+from django.core.files import File
+from django.core.management import call_command
+from django.utils.six import StringIO
+
+from misago.users.datadownloads import request_user_data_download
+from misago.users.management.commands import expireuserdatadownloads
+from misago.users.models import DataDownload
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
+TEST_FILE_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
+
+
+class ExpireUserDataDownloadsTests(AuthenticatedUserTestCase):
+    def test_delete_expired_data_download(self):
+        """management command deletes expired data download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+        with open(TEST_FILE_PATH, 'rb') as download_file:
+            data_download.file = File(download_file)
+            data_download.save()
+
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 1")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_EXPIRED)
+        self.assertFalse(updated_data_download.file)
+
+    def test_skip_not_expired_data_download(self):
+        """management command skips data download that expires in future"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+        data_download.expires_on += timedelta(hours=1)
+        with open(TEST_FILE_PATH, 'rb') as download_file:
+            data_download.file = File(download_file)
+            data_download.save()
+
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_READY)
+        self.assertTrue(updated_data_download.file)
+    
+    def test_skip_pending_data_download(self):
+        """management command skips pending data downloads"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_PENDING
+        data_download.save()
+
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_PENDING)
+
+    def test_skip_processing_data_download(self):
+        """management command skips processing data downloads"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_PROCESSING
+        data_download.save()
+
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_PROCESSING)
+
+    def test_skip_expired_data_download(self):
+        """management command skips pending data downloads"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_EXPIRED
+        data_download.save()
+
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_EXPIRED)
+
+    def test_no_expired_data_download(self):
+        """management command doesn't error when no data is expired"""
+        out = StringIO()
+        call_command(expireuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads expired: 0")

+ 0 - 20
misago/users/tests/test_gender_profilefield.py

@@ -181,11 +181,6 @@ class GenderProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
@@ -216,11 +211,6 @@ class GenderProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
@@ -242,11 +232,6 @@ class GenderProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
@@ -267,11 +252,6 @@ class GenderProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]

+ 53 - 18
misago/users/tests/test_lastip_profilefield.py → misago/users/tests/test_joinip_profilefield.py

@@ -9,9 +9,9 @@ from misago.acl.testutils import override_acl
 UserModel = get_user_model()
 
 
-class LastIpProfileFieldTests(AdminTestCase):
+class JoinIpProfileFieldTests(AdminTestCase):
     def setUp(self):
-        super(LastIpProfileFieldTests, self).setUp()
+        super(JoinIpProfileFieldTests, self).setUp()
 
         self.test_link = reverse(
             'misago:admin:users:accounts:edit',
@@ -23,9 +23,9 @@ class LastIpProfileFieldTests(AdminTestCase):
     def test_field_hidden_in_admin(self):
         """readonly field doesn't display in the admin"""
         response = self.client.get(self.test_link)
-        self.assertNotContains(response, 'name="last_ip"')
+        self.assertNotContains(response, 'name="join_ip"')
         self.assertNotContains(response, "IP address")
-        self.assertNotContains(response, "Last IP")
+        self.assertNotContains(response, "Join IP")
 
     def test_admin_edits_field(self):
         """admin form allows admins to edit field"""
@@ -36,7 +36,7 @@ class LastIpProfileFieldTests(AdminTestCase):
                 'rank': six.text_type(self.user.rank_id),
                 'roles': six.text_type(self.user.roles.all()[0].pk),
                 'email': 'reg@stered.com',
-                'last_ip': '127.0.0.1',
+                'join_ip': '127.0.0.1',
                 'new_password': '',
                 'signature': '',
                 'is_signature_locked': '0',
@@ -51,7 +51,7 @@ class LastIpProfileFieldTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         self.reload_user()
-        self.assertNotIn('last_ip', self.user.profile_fields)
+        self.assertNotIn('join_ip', self.user.profile_fields)
 
     def test_admin_search_field(self):
         """admin users search searches this field"""
@@ -72,17 +72,44 @@ class LastIpProfileFieldTests(AdminTestCase):
 
         response = self.client.get(test_link)
         self.assertContains(response, "IP address")
-        self.assertContains(response, "Last IP")
+        self.assertContains(response, "Join IP")
         self.assertContains(response, "127.0.0.1")
 
-        # IP fields tests ACL before displaying
+    def test_field_hidden_no_permission(self):
+        """field is hidden on user profile if user has no permission"""
+        test_link = reverse(
+            'misago:user-details',
+            kwargs={
+                'pk': self.user.pk,
+                'slug': self.user.slug,
+            },
+        )
+
         override_acl(self.user, {
             'can_see_users_ips': 0
         })
 
         response = self.client.get(test_link)
         self.assertNotContains(response, "IP address")
-        self.assertNotContains(response, "Last IP")
+        self.assertNotContains(response, "Join IP")
+        self.assertNotContains(response, "127.0.0.1")
+
+    def test_field_hidden_ip_removed(self):
+        """field is hidden on user profile if ip is removed"""
+        test_link = reverse(
+            'misago:user-details',
+            kwargs={
+                'pk': self.user.pk,
+                'slug': self.user.slug,
+            },
+        )
+
+        self.user.joined_from_ip = None
+        self.user.save()
+
+        response = self.client.get(test_link)
+        self.assertNotContains(response, "IP address")
+        self.assertNotContains(response, "Join IP")
         self.assertNotContains(response, "127.0.0.1")
 
     def test_field_display_json(self):
@@ -101,17 +128,15 @@ class LastIpProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
         )
 
-        # IP fields tests ACL before displaying
+    def test_field_hidden_no_permission_json(self):
+        """field is not included in display json if user has no permission"""
+        test_link = reverse('misago:api:user-details', kwargs={'pk': self.user.pk})
+
         override_acl(self.user, {
             'can_see_users_ips': 0
         })
@@ -119,6 +144,16 @@ class LastIpProfileFieldTests(AdminTestCase):
         response = self.client.get(test_link)
         self.assertEqual(response.json()['groups'], [])
 
+    def test_field_hidden_ip_removed_json(self):
+        """field is not included in display json if user ip is removed"""
+        test_link = reverse('misago:api:user-details', kwargs={'pk': self.user.pk})
+
+        self.user.joined_from_ip = None
+        self.user.save()
+
+        response = self.client.get(test_link)
+        self.assertEqual(response.json()['groups'], [])
+
     def test_field_not_in_edit_json(self):
         """readonly field json is not returned from API"""
         test_link = reverse('misago:api:user-edit-details', kwargs={'pk': self.user.pk})
@@ -128,7 +163,7 @@ class LastIpProfileFieldTests(AdminTestCase):
         found_field = None
         for group in response.json():
             for field in group['fields']:
-                if field['fieldname'] == 'last_ip':
+                if field['fieldname'] == 'join_ip':
                     found_field = field
 
         self.assertIsNone(found_field)
@@ -137,8 +172,8 @@ class LastIpProfileFieldTests(AdminTestCase):
         """readonly field can't be edited via api"""
         test_link = reverse('misago:api:user-edit-details', kwargs={'pk': self.user.pk})
 
-        response = self.client.post(test_link, data={'last_ip': '88.12.13.14'})
+        response = self.client.post(test_link, data={'join_ip': '88.12.13.14'})
         self.assertEqual(response.status_code, 200)
 
         self.reload_user()
-        self.assertNotIn('last_ip', self.user.profile_fields)
+        self.assertNotIn('join_ip', self.user.profile_fields)

+ 7 - 11
misago/users/tests/test_namechanges.py

@@ -1,7 +1,7 @@
 from django.contrib.auth import get_user_model
 from django.test import TestCase
 
-from misago.users.namechanges import get_available_namechanges_data
+from misago.users.namechanges import UsernameChanges
 
 
 UserModel = get_user_model()
@@ -12,21 +12,17 @@ class UsernameChangesTests(TestCase):
         """username changes are tracked correctly"""
         test_user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
 
-        namechanges = get_available_namechanges_data(test_user)
-        self.assertEqual(namechanges, {
-            'changes_left': 2,
-            'next_change_on': None,
-        })
+        namechanges = UsernameChanges(test_user)
+        self.assertEqual(namechanges.left, 2)
+        self.assertIsNone(namechanges.next_on)
 
         self.assertEqual(test_user.namechanges.count(), 0)
 
         test_user.set_username('Boberson')
         test_user.save(update_fields=['username', 'slug'])
 
-        namechanges = get_available_namechanges_data(test_user)
-        self.assertEqual(namechanges, {
-            'changes_left': 1,
-            'next_change_on': None,
-        })
+        namechanges = UsernameChanges(test_user)
+        self.assertEqual(namechanges.left, 1)
+        self.assertIsNone(namechanges.next_on)
 
         self.assertEqual(test_user.namechanges.count(), 1)

+ 4 - 7
misago/users/tests/test_options_views.py

@@ -29,13 +29,10 @@ class ConfirmChangeEmailTests(AuthenticatedUserTestCase):
         link = '/api/users/%s/change-email/' % self.user.pk
 
         response = self.client.post(
-            link,
-            data={
-                'new_email': 'n3w@email.com',
-                'password': self.USER_PASSWORD,
-            },
+            link, data={'new_email': 'n3w@email.com',
+                        'password': self.USER_PASSWORD}
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
             if line.startswith('http://'):
@@ -77,7 +74,7 @@ class ConfirmChangePasswordTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
             if line.startswith('http://'):

+ 84 - 0
misago/users/tests/test_prepareuserdatadownloads.py

@@ -0,0 +1,84 @@
+from django.core import mail
+from django.core.management import call_command
+from django.utils.six import StringIO
+
+from misago.conf import settings
+from misago.users.datadownloads import request_user_data_download
+from misago.users.management.commands import prepareuserdatadownloads
+from misago.users.models import DataDownload
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class PrepareUserDataDownloadsTests(AuthenticatedUserTestCase):
+    def test_process_pending_data_download(self):
+        """management command processes pending data download"""
+        data_download = request_user_data_download(self.user)
+        self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
+
+        out = StringIO()
+        call_command(prepareuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads prepared: 1")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_READY)
+        self.assertTrue(updated_data_download.expires_on > data_download.expires_on)
+        self.assertTrue(updated_data_download.file)
+
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].subject, "TestUser, your data download is ready")
+
+        absolute_url = ''.join([settings.MISAGO_ADDRESS.rstrip('/'), updated_data_download.file.url])
+        self.assertIn(absolute_url, mail.outbox[0].body)
+
+    def test_skip_ready_data_download(self):
+        """management command skips ready data download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_READY
+        data_download.save()
+
+        out = StringIO()
+        call_command(prepareuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads prepared: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_READY)
+
+        self.assertEqual(len(mail.outbox), 0)
+
+    def test_skip_processing_data_download(self):
+        """management command skips processing data download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_PROCESSING
+        data_download.save()
+
+        out = StringIO()
+        call_command(prepareuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads prepared: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_PROCESSING)
+        
+        self.assertEqual(len(mail.outbox), 0)
+
+    def test_skip_expired_data_download(self):
+        """management command skips expired data download"""
+        data_download = request_user_data_download(self.user)
+        data_download.status = DataDownload.STATUS_EXPIRED
+        data_download.save()
+
+        out = StringIO()
+        call_command(prepareuserdatadownloads.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+
+        self.assertEqual(command_output, "Data downloads prepared: 0")
+
+        updated_data_download = DataDownload.objects.get(pk=data_download.pk)
+        self.assertEqual(updated_data_download.status, DataDownload.STATUS_EXPIRED)
+        
+        self.assertEqual(len(mail.outbox), 0)

+ 54 - 0
misago/users/tests/test_removeoldips.py

@@ -0,0 +1,54 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.core.management import call_command
+from django.test import TestCase, override_settings
+from django.utils import timezone
+from django.utils.six import StringIO
+
+from misago.users.management.commands import removeoldips
+
+
+UserModel = get_user_model()
+
+USER_IP = '31.41.51.65'
+
+
+class RemoveOldIpsTests(TestCase):
+    def test_removeoldips_recent_user(self):
+        """command is not removing user's IP if its recent"""
+        user = UserModel.objects.create_user('Bob', 'bob@bob.com', joined_from_ip=USER_IP)
+        
+        out = StringIO()
+        call_command(removeoldips.Command(), stdout=out)
+
+        user_joined_from_ip = UserModel.objects.get(pk=user.pk).joined_from_ip
+        self.assertEqual(user_joined_from_ip, USER_IP)
+    
+    def test_removeoldips_old_user(self):
+        """command removes user's IP if its old"""
+        joined_on_past = timezone.now() - timedelta(days=50)
+        user = UserModel.objects.create_user('Bob1', 'bob1@bob.com', joined_from_ip=USER_IP)
+        user.joined_on = joined_on_past
+        user.save()
+
+        out = StringIO()
+        call_command(removeoldips.Command(), stdout=out)
+
+        user_joined_from_ip = UserModel.objects.get(pk=user.pk).joined_from_ip
+        self.assertIsNone(user_joined_from_ip)
+
+    @override_settings(MISAGO_IP_STORE_TIME=None)
+    def test_not_removing_user_ip(self):
+        """command is not removing user's IP if removing is disabled"""
+        user = UserModel.objects.create_user('Bob1', 'bob1@bob.com', joined_from_ip=USER_IP)
+        
+        out = StringIO()
+        call_command(removeoldips.Command(), stdout=out)
+        command_output = out.getvalue().splitlines()[0].strip()
+        
+        self.assertEqual(command_output, "Old IP removal is disabled.")
+        
+        user_joined_from_ip = UserModel.objects.get(pk=user.pk).joined_from_ip
+        self.assertEqual(user_joined_from_ip, USER_IP)
+

+ 24 - 24
misago/users/tests/test_search.py

@@ -27,10 +27,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -39,10 +39,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -51,10 +51,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=%s' % (self.api_link, self.user.username[0]))
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -65,10 +65,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=%s' % (self.api_link, self.user.username))
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -79,10 +79,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=%s' % (self.api_link, self.user.username[-3:]))
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)
@@ -93,10 +93,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=BobBoberson' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -112,10 +112,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=DisabledUser' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 self.assertEqual(provider['results']['results'], [])
 
@@ -126,10 +126,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         response = self.client.get('%s?q=DisabledUser' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIn('users', [p['id'] for p in response_json])
+        reponse_json = response.json()
+        self.assertIn('users', [p['id'] for p in reponse_json])
 
-        for provider in response_json:
+        for provider in reponse_json:
             if provider['id'] == 'users':
                 results = provider['results']['results']
                 self.assertEqual(len(results), 1)

+ 121 - 1
misago/users/tests/test_social_pipeline.py

@@ -8,6 +8,7 @@ from social_core.backends.github import GithubOAuth2
 from social_django.utils import load_strategy
 
 from misago.core.exceptions import SocialAuthFailed, SocialAuthBanned
+from misago.legal.models import Agreement
 
 from misago.users.models import AnonymousUser, Ban, BanCache
 from misago.users.social.pipeline import (
@@ -27,7 +28,7 @@ def create_request(user_ip='0.0.0.0', data=None):
     else:
         request = factory.post('/', data=json.dumps(data), content_type='application/json')
     request.include_frontend_context = True
-    request.frontend_context = {'auth': {}, 'conf': {}, 'store': {}, 'url': {}}
+    request.frontend_context = {}
     request.session = {}
     request.user = AnonymousUser()
     request.user_ip = user_ip
@@ -68,6 +69,8 @@ class PipelineTestCase(UserTestCase):
         if activation == 'admin':
             self.assertEqual(new_user.requires_activation, UserModel.ACTIVATION_ADMIN)
 
+        self.assertEqual(new_user.audittrail_set.count(), 1)
+
     def assertJsonResponseEquals(self, response, value):
         response_content = response.content.decode("utf-8")
         response_json = json.loads(response_content)
@@ -226,6 +229,16 @@ class CreateUser(PipelineTestCase):
 
 
 class CreateUserWithFormTests(PipelineTestCase):
+    def setUp(self):
+        super(CreateUserWithFormTests, self).setUp()
+
+        Agreement.objects.invalidate_cache()
+
+    def tearDown(self):
+        super(CreateUserWithFormTests, self).tearDown()
+        
+        Agreement.objects.invalidate_cache()
+
     def test_skip_if_user_is_set(self):
         """pipeline step is skipped if user was passed"""
         request = create_request()
@@ -441,6 +454,113 @@ class CreateUserWithFormTests(PipelineTestCase):
 
         self.assertNewUserIsCorrect(new_user, form_data, activation='admin', email_verified=False)
 
+    def test_form_check_agreement(self):
+        """social register checks agreement"""
+        form_data = {
+            'email': 'social@auth.com',
+            'username': 'SocialUser',
+        }
+        request = create_request(data=form_data)
+        strategy = load_strategy(request=request)
+        backend = GithubOAuth2(strategy, '/')
+
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text="Lorem ipsum",
+            is_active=True,
+        )
+
+        response = create_user_with_form(
+            strategy=strategy,
+            details={'email': form_data['email']},
+            backend=backend,
+            user=None,
+            pipeline_index=1,
+        )
+        
+        self.assertEqual(response.status_code, 400)
+        self.assertJsonResponseEquals(response, {
+            'terms_of_service': ['This agreement is required.'],
+        })
+
+        # invalid agreement id
+        form_data = {
+            'email': 'social@auth.com',
+            'username': 'SocialUser',
+            'terms_of_service': agreement.id + 1,
+        }
+        request = create_request(data=form_data)
+        strategy = load_strategy(request=request)
+        backend = GithubOAuth2(strategy, '/')
+
+        response = create_user_with_form(
+            strategy=strategy,
+            details={'email': form_data['email']},
+            backend=backend,
+            user=None,
+            pipeline_index=1,
+        )
+        
+        self.assertEqual(response.status_code, 400)
+        self.assertJsonResponseEquals(response, {
+            'terms_of_service': ['This agreement is required.'],
+        })
+
+        # valid agreement id
+        form_data = {
+            'email': 'social@auth.com',
+            'username': 'SocialUser',
+            'terms_of_service': agreement.id,
+        }
+        request = create_request(data=form_data)
+        strategy = load_strategy(request=request)
+        backend = GithubOAuth2(strategy, '/')
+
+        result = create_user_with_form(
+            strategy=strategy,
+            details={'email': form_data['email']},
+            backend=backend,
+            user=None,
+            pipeline_index=1,
+        )
+
+        new_user = UserModel.objects.get(email='social@auth.com')
+        self.assertEqual(result, {'user': new_user, 'is_new': True})
+
+        self.assertEqual(new_user.agreements, [agreement.id])
+        self.assertEqual(new_user.useragreement_set.count(), 1)
+
+    def test_form_ignore_inactive_agreement(self):
+        """social register ignores inactive agreement"""
+        form_data = {
+            'email': 'social@auth.com',
+            'username': 'SocialUser',
+            'terms_of_service': None,
+        }
+        request = create_request(data=form_data)
+        strategy = load_strategy(request=request)
+        backend = GithubOAuth2(strategy, '/')
+
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text="Lorem ipsum",
+            is_active=False,
+        )
+
+        result = create_user_with_form(
+            strategy=strategy,
+            details={'email': form_data['email']},
+            backend=backend,
+            user=None,
+            pipeline_index=1,
+        )
+
+        new_user = UserModel.objects.get(email='social@auth.com')
+        self.assertEqual(result, {'user': new_user, 'is_new': True})
+
+        self.assertEqual(new_user.agreements, [])
+        self.assertEqual(new_user.useragreement_set.count(), 0)
+
 
 class GetUsernameTests(PipelineTestCase):
     def test_skip_if_user_is_set(self):

+ 10 - 10
misago/users/tests/test_testutils.py

@@ -34,8 +34,8 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], user.id)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], user.id)
 
     def test_login_superuser(self):
         """login_user logs superuser"""
@@ -45,8 +45,8 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], user.id)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], user.id)
 
     def test_logout_user(self):
         """logout_user logs user out"""
@@ -57,8 +57,8 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
     def test_logout_superuser(self):
         """logout_user logs superuser out"""
@@ -69,8 +69,8 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertIsNone(response_json['id'])
+        user_json = response.json()
+        self.assertIsNone(user_json['id'])
 
 
 class AuthenticatedUserTestCaseTests(AuthenticatedUserTestCase):
@@ -96,5 +96,5 @@ class SuperUserTestCaseTests(SuperUserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['id'], self.user.id)
+        user_json = response.json()
+        self.assertEqual(user_json['id'], self.user.id)

+ 0 - 10
misago/users/tests/test_twitter_profilefield.py

@@ -155,11 +155,6 @@ class TwitterProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]
@@ -191,11 +186,6 @@ class TwitterProfileFieldTests(AdminTestCase):
                             'name': 'Join IP',
                             'text': '127.0.0.1',
                         },
-                        {
-                            'fieldname': 'last_ip',
-                            'name': 'Last IP',
-                            'text': '127.0.0.1',
-                        },
                     ],
                 },
             ]

+ 27 - 74
misago/users/tests/test_user_avatar_api.py

@@ -24,16 +24,6 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         super(UserAvatarTests, self).setUp()
         self.link = '/api/users/%s/avatar/' % self.user.pk
 
-    def assertAvatarChanged(self, response, detail):
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json()['detail'], detail)
-
-        old_avatars = self.user.avatars
-
-        self.reload_user()
-        self.assertEqual(response.json()['avatars'], self.user.avatars)
-        self.assertNotEqual(response.json()['avatars'], old_avatars)
-
     def get_current_user(self):
         return UserModel.objects.get(pk=self.user.pk)
 
@@ -82,50 +72,34 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Your avatar is locked. You can't change it.",
-            'extra': "<p>Your avatar is pwnt.</p>",
-        })
+        self.assertContains(response, "Your avatar is pwnt", status_code=403)
 
     def test_other_user_avatar(self):
         """requests to api error if user tries to access other user"""
         self.logout_user()
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You have to sign in to perform this action.",
-        })
+        self.assertContains(response, "You have to sign in", status_code=403)
 
         self.login_user(
             UserModel.objects.create_user("BobUser", "bob@bob.com", self.USER_PASSWORD)
         )
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't change other users avatars.",
-        })
+        self.assertContains(response, "can't change other users avatars", status_code=403)
 
     def test_empty_requests(self):
         """empty request errors with code 400"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Unknown avatar type.",
-        })
-        
+        self.assertContains(response, "Unknown avatar type.", status_code=400)
+
     def test_failed_gravatar_request(self):
         """no gravatar RPC fails"""
         self.user.email_hash = 'wolololo'
         self.user.save()
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "No Gravatar is associated with your e-mail address.",
-        })
+        self.assertContains(response, "No Gravatar is associated", status_code=400)
 
     def test_successful_gravatar_request(self):
         """gravatar RPC passes"""
@@ -133,33 +107,29 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertAvatarChanged(response, "Gravatar was downloaded and set as new avatar.")
+        self.assertContains(response, "Gravatar was downloaded and set")
 
     def test_generation_request(self):
         """generated avatar is set"""
         response = self.client.post(self.link, data={'avatar': 'generated'})
-        self.assertAvatarChanged(response, "New avatar based on your account was set.")
+        self.assertContains(response, "New avatar based on your account")
 
     def test_avatar_upload_and_crop(self):
         """avatar can be uploaded and cropped"""
         response = self.client.post(self.link, data={'avatar': 'generated'})
         self.assertEqual(response.status_code, 200)
-        self.assertAvatarChanged(response, "New avatar based on your account was set.")
 
         response = self.client.post(self.link, data={'avatar': 'upload'})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "No file was sent.",  # fixme: detail with status 400 is no no
-        })
+        self.assertContains(response, "No file was sent.", status_code=400)
 
         with open(TEST_AVATAR_PATH, 'rb') as avatar:
             response = self.client.post(self.link, data={'avatar': 'upload', 'image': avatar})
             self.assertEqual(response.status_code, 200)
 
-            avatar_json = response.json()
-            self.assertTrue(avatar_json['crop_tmp'])
+            response_json = response.json()
+            self.assertTrue(response_json['crop_tmp'])
             self.assertEqual(
-                self.get_current_user().avatar_tmp.url, avatar_json['crop_tmp']['url']
+                self.get_current_user().avatar_tmp.url, response_json['crop_tmp']['url']
             )
 
         avatar = Path(self.get_current_user().avatar_tmp.path)
@@ -181,8 +151,9 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             content_type="application/json",
         )
 
+        response_json = response.json()
         self.assertEqual(response.status_code, 200)
-        self.assertAvatarChanged(response, "Uploaded avatar was set.")
+        self.assertContains(response, "Uploaded avatar was set.")
 
         self.assertFalse(self.get_current_user().avatar_tmp)
 
@@ -204,10 +175,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "This avatar type is not allowed.",
-        })
+        self.assertContains(response, "This avatar type is not allowed.", status_code=400)
 
         response = self.client.post(
             self.link,
@@ -223,7 +191,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             }),
             content_type="application/json",
         )
-        self.assertAvatarChanged(response, "Avatar was re-cropped.")
+        self.assertContains(response, "Avatar was re-cropped.")
 
         # delete user avatars, test if it deletes src and tmp
         store.delete_avatar(self.get_current_user())
@@ -240,10 +208,8 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         response = self.client.post(self.link, data={'avatar': 'galleries', 'image': 123})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "This avatar type is not allowed.",
-        })
+
+        self.assertContains(response, "This avatar type is not allowed.", status_code=400)
 
     def test_gallery_image_validation(self):
         """gallery validates image to set"""
@@ -259,10 +225,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                 'avatar': 'galleries',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Incorrect image.",
-        })
+        self.assertContains(response, "Incorrect image.", status_code=400)
 
         # invalid id is handled
         response = self.client.post(
@@ -272,10 +235,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                 'image': 'asdsadsadsa',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Incorrect image.",
-        })
+        self.assertContains(response, "Incorrect image.", status_code=400)
 
         # nonexistant image is handled
         response = self.client.get(self.link)
@@ -292,19 +252,13 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                 'image': test_avatar + 5000,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Incorrect image.",
-        })
+        self.assertContains(response, "Incorrect image.", status_code=400)
 
         # default gallery image is handled
         AvatarGallery.objects.filter(pk=test_avatar).update(gallery=gallery.DEFAULT_GALLERY)
 
         response = self.client.post(self.link, data={'avatar': 'galleries', 'image': test_avatar})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Incorrect image.",
-        })
+        self.assertContains(response, "Incorrect image.", status_code=400)
 
     def test_gallery_set_valid_avatar(self):
         """its possible to set avatar from gallery"""
@@ -324,7 +278,8 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                 'image': test_avatar,
             },
         )
-        self.assertAvatarChanged(response, "Avatar from gallery was set.")
+
+        self.assertContains(response, "Avatar from gallery was set.")
 
 
 class UserAvatarModerationTests(AuthenticatedUserTestCase):
@@ -344,10 +299,7 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't moderate avatars.",
-        })
+        self.assertContains(response, "can't moderate avatars", status_code=403)
 
     def test_moderate_avatar(self):
         """moderate avatar"""
@@ -383,11 +335,12 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
+
+        options = response.json()
         self.assertEqual(other_user.is_avatar_locked, True)
         self.assertEqual(other_user.avatar_lock_user_message, "Test user message.")
         self.assertEqual(other_user.avatar_lock_staff_message, "Test staff message.")
 
-        options = response.json()
         self.assertEqual(options['avatars'], other_user.avatars)
         self.assertEqual(options['is_avatar_locked'], other_user.is_avatar_locked)
         self.assertEqual(options['avatar_lock_user_message'], other_user.avatar_lock_user_message)

+ 10 - 15
misago/users/tests/test_user_changeemail_api.py

@@ -23,12 +23,13 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
     def test_empty_input(self):
         """api errors correctly for empty input"""
         response = self.client.post(self.link, data={})
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
                 'new_email': ["This field is required."],
                 'password': ["This field is required."],
-            },
+            }
         )
 
     def test_invalid_password(self):
@@ -40,12 +41,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': 'Lor3mIpsum',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'password': ["Entered password is invalid."],
-            },
-        )
+        self.assertContains(response, 'password is invalid', status_code=400)
 
     def test_invalid_input(self):
         """api errors correctly for invalid input"""
@@ -56,6 +52,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
             'new_email': ["This field may not be blank."],
@@ -68,6 +65,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
             'new_email': ["Enter a valid email address."],
@@ -84,12 +82,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {
-                'new_email': ["This e-mail address is not available."],
-            },
-        )
+        self.assertContains(response, 'not available', status_code=400)
 
     def test_change_email(self):
         """api allows users to change their e-mail addresses"""
@@ -102,7 +95,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn('Confirm e-mail change', mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -129,6 +122,8 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
     def test_change_email_user_password_whitespace(self):
         """api supports users with whitespace around their passwords"""
         user_password = ' old password '
+        new_password = ' N3wP@55w0rd '
+
         new_email = 'new@email.com'
 
         self.user.set_password(user_password)
@@ -143,7 +138,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
                 'password': user_password,
             },
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn('Confirm e-mail change', mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:

+ 2 - 2
misago/users/tests/test_user_changepassword_api.py

@@ -87,7 +87,7 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
                 'password': self.USER_PASSWORD,
             },
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn('Confirm password change', mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -125,7 +125,7 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
                 'password': old_password,
             },
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn('Confirm password change', mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:

+ 129 - 73
misago/users/tests/test_user_create_api.py

@@ -4,6 +4,7 @@ from django.test import override_settings
 from django.urls import reverse
 
 from misago.conf import settings
+from misago.legal.models import Agreement
 from misago.users.models import Ban, Online
 from misago.users.testutils import UserTestCase
 
@@ -16,17 +17,18 @@ class UserCreateTests(UserTestCase):
 
     def setUp(self):
         super(UserCreateTests, self).setUp()
+        
+        Agreement.objects.invalidate_cache()
+
         self.api_link = '/api/users/'
 
+    def tearDown(self):
+        Agreement.objects.invalidate_cache()
+
     def test_empty_request(self):
         """empty request errors with code 400"""
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'email': ["This field is required."],
-            'password': ["This field is required."],
-            'username': ["This field is required."],
-        })
 
     def test_invalid_data(self):
         """invalid request data errors with code 400"""
@@ -36,28 +38,19 @@ class UserCreateTests(UserTestCase):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
-        })
 
     def test_authenticated_request(self):
         """authentiated user request errors with code 403"""
         self.login_user(self.get_authenticated_user())
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to signed in users.",
-        })
 
     def test_registration_off_request(self):
         """registrations off request errors with code 403"""
         settings.override_setting('account_activation', 'closed')
 
         response = self.client.post(self.api_link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "New users registrations are currently closed.",
-        })
+        self.assertContains(response, 'closed', status_code=403)
 
     def test_registration_validates_ip_ban(self):
         """api validates ip ban"""
@@ -75,14 +68,8 @@ class UserCreateTests(UserTestCase):
                 'password': 'LoremP4ssword',
             },
         )
+
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': {
-                'html': '<p>You can&#39;t register account like this.</p>',
-                'plain': "You can't register account like this."
-            },
-            'expires_on': None,
-        })
 
     def test_registration_validates_ip_registration_ban(self):
         """api validates ip registration-only ban"""
@@ -102,14 +89,10 @@ class UserCreateTests(UserTestCase):
             },
         )
 
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
-                'detail': {
-                    'html': '<p>You can&#39;t register account like this.</p>',
-                    'plain': "You can't register account like this.",
-                },
-                'expires_on': None,
+                '__all__': ["You can't register account like this."],
             }
         )
 
@@ -170,6 +153,7 @@ class UserCreateTests(UserTestCase):
                 'password': 'LoremP4ssword',
             },
         )
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
@@ -211,7 +195,7 @@ class UserCreateTests(UserTestCase):
                 'password': 'LoremP4ssword',
             },
         )
-        
+
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
             'email': ["You can't register account like this."],
@@ -250,14 +234,10 @@ class UserCreateTests(UserTestCase):
                 'password': '123',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'password': [
-                "This password is too short. It must contain at least 7 characters.",
-                "This password is entirely numeric.",
-            ],
-            'email': ["This email is not allowed."],
-        })
+
+        self.assertContains(response, "password is too short", status_code=400)
+        self.assertContains(response, "password is entirely numeric", status_code=400)
+        self.assertContains(response, "email is not allowed", status_code=400)
 
     def test_registration_validates_password_similiarity(self):
         """api uses validate_password to validate registrations"""
@@ -269,11 +249,8 @@ class UserCreateTests(UserTestCase):
                 'password': 'BobBoberson',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'password': ["The password is too similar to the username."],
-            'email': ["This email is not allowed."],
-        })
+
+        self.assertContains(response, "password is too similar to the username", status_code=400)
 
     @override_settings(captcha_type='qa', qa_question='Test', qa_answers='Lorem\nIpsum')
     def test_registration_validates_captcha(self):
@@ -308,6 +285,89 @@ class UserCreateTests(UserTestCase):
 
         self.assertEqual(response.status_code, 200)
 
+    def test_registration_check_agreement(self):
+        """api checks agreement"""
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text="Lorem ipsum",
+            is_active=True,
+        )
+
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'totallyNew',
+                'email': 'loremipsum@dolor.met',
+                'password': 'LoremP4ssword',
+            },
+        )
+
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(
+            response.json(), {
+                'terms_of_service': ['This agreement is required.'],
+            }
+        )
+
+        # invalid agreement id
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'totallyNew',
+                'email': 'loremipsum@dolor.met',
+                'password': 'LoremP4ssword',
+                'terms_of_service': agreement.id + 1,
+            },
+        )
+
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(
+            response.json(), {
+                'terms_of_service': ['This agreement is required.'],
+            }
+        )
+
+        # valid agreement id
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'totallyNew',
+                'email': 'loremipsum@dolor.met',
+                'password': 'LoremP4ssword',
+                'terms_of_service': agreement.id,
+            },
+        )
+
+        self.assertEqual(response.status_code, 200)
+        
+        user = UserModel.objects.get(email='loremipsum@dolor.met')
+        self.assertEqual(user.agreements, [agreement.id])
+        self.assertEqual(user.useragreement_set.count(), 1)
+
+    def test_registration_ignore_inactive_agreement(self):
+        """api ignores inactive agreement"""
+        Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            text="Lorem ipsum",
+            is_active=False,
+        )
+
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'totallyNew',
+                'email': 'loremipsum@dolor.met',
+                'password': 'LoremP4ssword',
+                'terms_of_service': '',
+            },
+        )
+
+        self.assertEqual(response.status_code, 200)
+        
+        user = UserModel.objects.get(email='loremipsum@dolor.met')
+        self.assertEqual(user.agreements, [])
+        self.assertEqual(user.useragreement_set.count(), 0)
+
     def test_registration_calls_validate_new_registration(self):
         """api uses validate_new_registration to validate registrations"""
         response = self.client.post(
@@ -315,13 +375,11 @@ class UserCreateTests(UserTestCase):
             data={
                 'username': 'Bob',
                 'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
-                'password': 'pass1234',
+                'password': 'pas123',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'email': ["This email is not allowed."]
-        })
+
+        self.assertContains(response, "email is not allowed", status_code=400)
 
     def test_registration_creates_active_user(self):
         """api creates active and signed in user on POST"""
@@ -336,11 +394,9 @@ class UserCreateTests(UserTestCase):
             },
         )
 
-        self.assertEqual(response.json(), {
-            'activation': None,
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-        })
+        self.assertContains(response, 'active')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
 
         UserModel.objects.get_by_username('Bob')
 
@@ -348,12 +404,15 @@ class UserCreateTests(UserTestCase):
         self.assertEqual(Online.objects.filter(user=test_user).count(), 1)
 
         self.assertTrue(test_user.check_password('pass123'))
-
-        response = self.client.get(reverse('misago:index'))
-        self.assertContains(response, 'Bob')
+        
+        auth_json = self.client.get(reverse('misago:api:auth')).json()
+        self.assertTrue(auth_json['is_authenticated'])
+        self.assertEqual(auth_json['username'], 'Bob')
 
         self.assertIn('Welcome', mail.outbox[0].subject)
 
+        self.assertEqual(test_user.audittrail_set.count(), 1)
+
     def test_registration_creates_inactive_user(self):
         """api creates inactive user on POST"""
         settings.override_setting('account_activation', 'user')
@@ -367,11 +426,12 @@ class UserCreateTests(UserTestCase):
             },
         )
 
-        self.assertEqual(response.json(), {
-            'activation': 'user',
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-        })
+        self.assertContains(response, 'user')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
+
+        auth_json = self.client.get(reverse('misago:api:auth')).json()
+        self.assertFalse(auth_json['is_authenticated'])
 
         UserModel.objects.get_by_username('Bob')
         UserModel.objects.get_by_email('bob@bob.com')
@@ -391,11 +451,12 @@ class UserCreateTests(UserTestCase):
             },
         )
 
-        self.assertEqual(response.json(), {
-            'activation': 'admin',
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-        })
+        self.assertContains(response, 'admin')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
+
+        auth_json = self.client.get(reverse('misago:api:auth')).json()
+        self.assertFalse(auth_json['is_authenticated'])
 
         UserModel.objects.get_by_username('Bob')
         UserModel.objects.get_by_email('bob@bob.com')
@@ -415,11 +476,9 @@ class UserCreateTests(UserTestCase):
             },
         )
 
-        self.assertEqual(response.json(), {
-            'activation': None,
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-        })
+        self.assertContains(response, 'active')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
 
         UserModel.objects.get_by_username('Bob')
 
@@ -428,7 +487,4 @@ class UserCreateTests(UserTestCase):
 
         self.assertTrue(test_user.check_password(' pass123 '))
 
-        response = self.client.get(reverse('misago:index'))
-        self.assertContains(response, 'Bob')
-
         self.assertIn('Welcome', mail.outbox[0].subject)

+ 47 - 0
misago/users/tests/test_user_datadownloads_api.py

@@ -0,0 +1,47 @@
+from misago.users.datadownloads import request_user_data_download
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class UserDataDownloadsApiTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(UserDataDownloadsApiTests, self).setUp()
+        self.link = '/api/users/%s/data-downloads/' % self.user.pk
+
+    def test_get_other_user_exports_anonymous(self):
+        """requests to api fails if user is anonymous"""
+        self.logout_user()
+
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have to sign in to perform this action.",
+        })
+
+    def test_get_other_user_exports(self):
+        """requests to api fails if user tries to access other user"""
+        other_user = self.get_superuser()
+        link = '/api/users/%s/data-downloads/' % other_user.pk
+
+        response = self.client.get(link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't see other users data downloads.",
+        })
+        
+    def test_get_empty_list(self):
+        """api returns empy list"""
+        self.assertFalse(self.user.datadownload_set.exists())
+
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), [])
+
+    def test_populated_list(self):
+        """api returns list"""
+        for _ in range(6):
+            request_user_data_download(self.user)
+        self.assertTrue(self.user.datadownload_set.exists())
+
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.json()), 5)

+ 3 - 3
misago/users/tests/test_user_model.py

@@ -74,11 +74,11 @@ class UserManagerTests(TestCase):
 
 
 class UserModelTests(TestCase):
-    def test_anonymize_content(self):
-        """anonymize_content sets username and slug to one defined in settings"""
+    def test_anonymize_data(self):
+        """anonymize_data sets username and slug to one defined in settings"""
         user = User.objects.create_user('Bob', 'bob@example.com', 'Pass.123')
 
-        user.anonymize_content()
+        user.anonymize_data()
         self.assertEqual(user.username, settings.MISAGO_ANONYMOUS_USERNAME)
         self.assertEqual(user.slug, slugify(settings.MISAGO_ANONYMOUS_USERNAME))
 

+ 60 - 0
misago/users/tests/test_user_requestdatadownload_api.py

@@ -0,0 +1,60 @@
+from django.test.utils import override_settings
+
+from misago.users.datadownloads import request_user_data_download
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class UserRequestDataDownload(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(UserRequestDataDownload, self).setUp()
+        self.link = '/api/users/%s/request-data-download/' % self.user.pk
+
+    def test_request_other_user_download_anonymous(self):
+        """requests to api fails if user is anonymous"""
+        self.logout_user()
+
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This action is not available to guests.",
+        })
+
+    def test_request_other_user_download(self):
+        """requests to api fails if user tries to access other user"""
+        other_user = self.get_superuser()
+        link = '/api/users/%s/request-data-download/' % other_user.pk
+
+        response = self.client.post(link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't request data downloads for other users.",
+        })
+
+    @override_settings(MISAGO_ENABLE_DOWNLOAD_OWN_DATA=False)
+    def test_request_download_disabled(self):
+        """request to api fails if own data downloads are disabled"""
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't download your data.",
+        })
+
+    def test_request_download_in_progress(self):
+        """request to api fails if user has already requested data download"""
+        request_user_data_download(self.user)
+
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't have more than one data download request at single time.",
+        })
+
+    def test_request_download(self):
+        """request to api succeeds"""
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'detail': "ok",
+        })
+
+        self.assertTrue(self.user.datadownload_set.exists())

+ 13 - 29
misago/users/tests/test_user_signature_api.py

@@ -16,10 +16,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to change signature.",
-        })
+        self.assertContains(response, "You don't have permission to change", status_code=403)
 
     def test_signature_locked(self):
         """locked edit signature returns 403"""
@@ -32,11 +29,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "Your signature is locked. You can't change it.",
-            'extra': "<p>Your siggy is banned.</p>",
-        })
+        self.assertContains(response, 'Your siggy is banned', status_code=403)
 
     def test_get_signature(self):
         """GET to api returns json with no signature"""
@@ -49,10 +42,8 @@ class UserSignatureTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'signature': None,
-            'limit': 256,
-        })
+
+        self.assertFalse(response.json()['signature'])
 
     def test_post_empty_signature(self):
         """empty POST empties user signature"""
@@ -70,10 +61,8 @@ class UserSignatureTests(AuthenticatedUserTestCase):
             },
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'signature': None,
-            'limit': 256,
-        })
+
+        self.assertFalse(response.json()['signature'])
 
     def test_post_too_long_signature(self):
         """too long new signature errors"""
@@ -90,10 +79,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
                 'signature': 'abcd' * 1000,
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'non_field_errors': ["Signature is too long."],
-        })
+        self.assertContains(response, 'too long', status_code=400)
 
     def test_post_good_signature(self):
         """POST with good signature changes user signature"""
@@ -111,14 +97,12 @@ class UserSignatureTests(AuthenticatedUserTestCase):
             },
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'signature': {
-                'html': '<p>Hello, <strong>bros</strong>!</p>',
-                'plain': 'Hello, **bros**!',
-            },
-            'limit': 256,
-        })
 
-        # API updates user in database
+        self.assertEqual(
+            response.json()['signature']['html'], '<p>Hello, <strong>bros</strong>!</p>'
+        )
+        self.assertEqual(response.json()['signature']['plain'], 'Hello, **bros**!')
+
         self.reload_user()
+
         self.assertEqual(self.user.signature_parsed, '<p>Hello, <strong>bros</strong>!</p>')

+ 51 - 92
misago/users/tests/test_user_username_api.py

@@ -17,28 +17,27 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         super(UserUsernameTests, self).setUp()
         self.link = '/api/users/%s/username/' % self.user.pk
 
-    def test_get_change_username_form_options(self):
-        """get to API returns form options"""
+    def test_get_change_username_options(self):
+        """get to API returns options"""
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'changes_left': self.user.acl_cache['name_changes_allowed'],
-            'next_change_on': None,
-            'length_min': settings.username_length_min,
-            'length_max': settings.username_length_max,
-        })
 
-        for i in range(response.json()['changes_left']):
+        response_json = response.json()
+
+        self.assertIsNotNone(response_json['changes_left'])
+        self.assertEqual(response_json['length_min'], settings.username_length_min)
+        self.assertEqual(response_json['length_max'], settings.username_length_max)
+        self.assertIsNone(response_json['next_on'])
+
+        for i in range(response_json['changes_left']):
             self.user.set_username('NewName%s' % i, self.user)
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'changes_left': 0,
-            'next_change_on': response.json()['next_change_on'],
-            'length_min': settings.username_length_min,
-            'length_max': settings.username_length_max,
-        })
+
+        response_json = response.json()
+        self.assertEqual(response_json['changes_left'], 0)
+        self.assertIsNotNone(response_json['next_on'])
 
     def test_change_username_no_changes_left(self):
         """api returns error 400 if there are no username changes left"""
@@ -56,31 +55,18 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.post(
             self.link,
             data={
-                'username': 'FailedChanges',
+                'username': 'Pointless',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["You can't change your username at this time."],
-            'next_change_on': response.json()['next_change_on'],
-        })
-        
-        self.reload_user()
-        self.assertTrue(self.user.username != 'FailedChanges')
+
+        self.assertContains(response, 'change your username now', status_code=400)
+        self.assertTrue(self.user.username != 'Pointless')
 
     def test_change_username_no_input(self):
-        """api returns error 400 if new username is omitted or empty"""
+        """api returns error 400 if new username is empty"""
         response = self.client.post(self.link, data={})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["This field is required."],
-        })
 
-        response = self.client.post(self.link, data={'username': ''})
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["This field may not be blank."],
-        })
+        self.assertContains(response, 'Enter new username.', status_code=400)
 
     def test_change_username_invalid_name(self):
         """api returns error 400 if new username is wrong"""
@@ -90,13 +76,14 @@ class UserUsernameTests(AuthenticatedUserTestCase):
                 'username': '####',
             },
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["Username can only contain latin alphabet letters and digits."],
-        })
+
+        self.assertContains(response, 'can only contain latin', status_code=400)
 
     def test_change_username(self):
         """api changes username and records change"""
+        response = self.client.get(self.link)
+        changes_left = response.json()['changes_left']
+
         old_username = self.user.username
         new_username = 'NewUsernamu'
 
@@ -106,17 +93,14 @@ class UserUsernameTests(AuthenticatedUserTestCase):
                 'username': new_username,
             },
         )
+
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'username': 'NewUsernamu',
-            'slug': 'newusernamu',
-            'changes_left': 1,
-            'next_change_on': None,
-        })
+        options = response.json()['options']
+        self.assertEqual(changes_left, options['changes_left'] + 1)
 
         self.reload_user()
         self.assertEqual(self.user.username, new_username)
-        self.assertNotEqual(self.user.username, old_username)
+        self.assertTrue(self.user.username != old_username)
 
         self.assertEqual(self.user.namechanges.last().new_username, new_username)
 
@@ -138,38 +122,27 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't rename users."
-        })
+        self.assertContains(response, "can't rename users", status_code=403)
 
         override_acl(self.user, {
             'can_rename_users': 0,
         })
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't rename users."
-        })
+        self.assertContains(response, "can't rename users", status_code=403)
 
-    def test_invalid_username(self):
-        """moderate username api validates username"""
+    def test_moderate_username(self):
+        """moderate username"""
         override_acl(self.user, {
             'can_rename_users': 1,
         })
 
-        response = self.client.post(
-            self.link,
-            json.dumps({
-                'username': None,
-            }),
-            content_type='application/json',
-        )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["This field may not be null."],
-        })
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+
+        options = response.json()
+        self.assertEqual(options['length_min'], settings.username_length_min)
+        self.assertEqual(options['length_max'], settings.username_length_max)
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -182,10 +155,8 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["This field may not be blank."],
-        })
+
+        self.assertContains(response, "Enter new username", status_code=400)
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -198,10 +169,12 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["Username can only contain latin alphabet letters and digits."],
-        })
+
+        self.assertContains(
+            response,
+            "Username can only contain latin alphabet letters and digits.",
+            status_code=400
+        )
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -214,26 +187,12 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-        self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'username': ["Username must be at least 3 characters long."],
-        })
-
-    def test_get_username_requirements(self):
-        """get to API returns username requirements"""
-        override_acl(self.user, {
-            'can_rename_users': 1,
-        })
 
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'length_min': settings.username_length_min,
-            'length_max': settings.username_length_max,
-        })
+        self.assertEqual(response.status_code, 400)
+        self.assertContains(
+            response, "Username must be at least 3 characters long.", status_code=400
+        )
 
-    def test_moderate_username(self):
-        """moderate username"""
         override_acl(self.user, {
             'can_rename_users': 1,
         })

+ 144 - 30
misago/users/tests/test_useradmin_views.py

@@ -6,8 +6,11 @@ from django.utils import six
 from misago.acl.models import Role
 from misago.admin.testutils import AdminTestCase
 from misago.categories.models import Category
+from misago.legal.models import Agreement
+from misago.legal.utils import save_user_agreement_acceptance
 from misago.threads.testutils import post_thread, reply_thread
-from misago.users.models import Ban, Rank
+from misago.users.datadownloads import request_user_data_download
+from misago.users.models import Ban, DataDownload, Rank
 
 
 UserModel = get_user_model()
@@ -135,7 +138,45 @@ class UserAdminViewsTests(AdminTestCase):
                 'selected_items': user_pks,
             }
         )
-        self.assertEqual(response.status_code, 200)
+        self.assertNotContains(response, 'value="ip"')
+        self.assertNotContains(response, 'value="ip_first"')
+        self.assertNotContains(response, 'value="ip_two"')
+
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:index'),
+            data={
+                'action': 'ban',
+                'selected_items': user_pks,
+                'ban_type': ['usernames', 'emails', 'domains'],
+                'finalize': '',
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(Ban.objects.count(), 21)
+
+    def test_mass_ban_with_ips(self):
+        """users list bans multiple users that also have ips"""
+        user_pks = []
+        for i in range(10):
+            test_user = UserModel.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                joined_from_ip='73.95.67.27',
+                requires_activation=1,
+            )
+            user_pks.append(test_user.pk)
+
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:index'),
+            data={
+                'action': 'ban',
+                'selected_items': user_pks,
+            }
+        )
+        self.assertContains(response, 'value="ip"')
+        self.assertContains(response, 'value="ip_first"')
+        self.assertContains(response, 'value="ip_two"')
 
         response = self.client.post(
             reverse('misago:admin:users:accounts:index'),
@@ -149,6 +190,53 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Ban.objects.count(), 24)
 
+    def test_mass_request_data_download(self):
+        """users list requests data download for multiple users"""
+        user_pks = []
+        for i in range(10):
+            test_user = UserModel.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1,
+            )
+            user_pks.append(test_user.pk)
+
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:index'),
+            data={
+                'action': 'request_data_download',
+                'selected_items': user_pks,
+            }
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.filter(user_id__in=user_pks).count(), len(user_pks))
+
+    def test_mass_request_data_download_avoid_excessive_downloads(self):
+        """users list avoids excessive data download requests for multiple users"""
+        user_pks = []
+        for i in range(10):
+            test_user = UserModel.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1,
+            )
+            request_user_data_download(test_user)
+            user_pks.append(test_user.pk)
+
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:index'),
+            data={
+                'action': 'v',
+                'selected_items': user_pks,
+            }
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.filter(user_id__in=user_pks).count(), len(user_pks))
+
     def test_mass_delete_accounts_self(self):
         """its impossible to delete oneself"""
         user_pks = [self.user.pk]
@@ -955,6 +1043,32 @@ class UserAdminViewsTests(AdminTestCase):
         updated_user = UserModel.objects.get(pk=test_user.pk)
         self.assertFalse(updated_user.has_usable_password())
 
+    def test_edit_agreements_list(self):
+        """edit view displays list of user's agreements"""
+        test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
+
+        agreement = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            title="Test agreement!",
+            text="Lorem ipsum!",
+            is_active=True,
+        )
+
+        response = self.client.get(test_link)
+        self.assertEqual(response.status_code, 200)
+        self.assertNotContains(response, agreement.title)
+
+        save_user_agreement_acceptance(test_user, agreement, commit=True)
+
+        response = self.client.get(test_link)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, agreement.title)
+
     def test_delete_threads_view_self(self):
         """delete user threads view validates if user deletes self"""
         test_link = reverse(
@@ -967,7 +1081,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "delete yourself")
+        self.assertContains(response, "delete yourself");
 
     def test_delete_threads_view_staff(self):
         """delete user threads view validates if user deletes staff"""
@@ -985,7 +1099,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_threads_view_superuser(self):
         """delete user threads view validates if user deletes superuser"""
@@ -1003,7 +1117,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_threads_view(self):
         """delete user threads view deletes threads"""
@@ -1019,17 +1133,17 @@ class UserAdminViewsTests(AdminTestCase):
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'deleted_count': 10,
-            'is_completed': False,
-        })
+
+        response_dict = response.json()
+        self.assertEqual(response_dict['deleted_count'], 10)
+        self.assertFalse(response_dict['is_completed'])
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'deleted_count': 0,
-            'is_completed': True,
-        })
+
+        response_dict = response.json()
+        self.assertEqual(response_dict['deleted_count'], 0)
+        self.assertTrue(response_dict['is_completed'])
 
     def test_delete_posts_view_self(self):
         """delete user posts view validates if user deletes self"""
@@ -1043,7 +1157,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "delete yourself")
+        self.assertContains(response, "delete yourself");
 
     def test_delete_posts_view_staff(self):
         """delete user posts view validates if user deletes staff"""
@@ -1061,7 +1175,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_posts_view_superuser(self):
         """delete user posts view validates if user deletes superuser"""
@@ -1079,7 +1193,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_posts_view(self):
         """delete user posts view deletes posts"""
@@ -1096,17 +1210,17 @@ class UserAdminViewsTests(AdminTestCase):
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'deleted_count': 10,
-            'is_completed': False,
-        })
+
+        response_dict = response.json()
+        self.assertEqual(response_dict['deleted_count'], 10)
+        self.assertFalse(response_dict['is_completed'])
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'deleted_count': 0,
-            'is_completed': True,
-        })
+
+        response_dict = response.json()
+        self.assertEqual(response_dict['deleted_count'], 0)
+        self.assertTrue(response_dict['is_completed'])
 
     def test_delete_account_view_self(self):
         """delete user account view validates if user deletes self"""
@@ -1120,7 +1234,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "delete yourself")
+        self.assertContains(response, "delete yourself");
 
     def test_delete_account_view_staff(self):
         """delete user account view validates if user deletes staff"""
@@ -1138,7 +1252,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_account_view_superuser(self):
         """delete user account view validates if user deletes superuser"""
@@ -1156,7 +1270,7 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertContains(response, "is admin and")
+        self.assertContains(response, "is admin and");
 
     def test_delete_account_view(self):
         """delete user account view deletes user account"""
@@ -1169,6 +1283,6 @@ class UserAdminViewsTests(AdminTestCase):
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'is_completed': True,
-        })
+
+        response_dict = response.json()
+        self.assertTrue(response_dict['is_completed'])

+ 7 - 95
misago/users/tests/test_usernamechanges_api.py

@@ -1,5 +1,4 @@
 from misago.acl.testutils import override_acl
-from misago.core.utils import serialize_datetime
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
@@ -11,46 +10,12 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
     def test_user_can_always_see_his_name_changes(self):
         """list returns own username changes"""
         self.user.set_username('NewUsername', self.user)
-        self.user.save()
 
         override_acl(self.user, {'can_see_users_name_history': False})
 
         response = self.client.get('%s?user=%s' % (self.link, self.user.pk))
         self.assertEqual(response.status_code, 200)
-        
-        username_change = self.user.namechanges.all()[0]
-        self.assertEqual(response.json(), {
-            'page': 1,
-            'pages': 1,
-            'count': 1,
-            'first': None,
-            'previous': None,
-            'next': None,
-            'last': None,
-            'before': 0,
-            'more': 0,
-            'results': [
-                {
-                    'id': username_change.id,
-                    'user': {
-                        'id': self.user.id,
-                        'username': 'NewUsername',
-                        'slug': 'newusername',
-                        'avatars': self.user.avatars,
-                    },
-                    'changed_by': {
-                        'id': self.user.id,
-                        'username': 'NewUsername',
-                        'slug': 'newusername',
-                        'avatars': self.user.avatars,
-                    },
-                    'changed_by_username': 'NewUsername',
-                    'changed_on': serialize_datetime(username_change.changed_on),
-                    'new_username': 'NewUsername',
-                    'old_username': 'TestUser'
-                }
-            ],
-        })
+        self.assertContains(response, self.user.username)
 
     def test_list_handles_invalid_filter(self):
         """list raises 404 for invalid filter"""
@@ -60,7 +25,6 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get('%s?user=abcd' % self.link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_list_handles_nonexisting_user(self):
         """list raises 404 for invalid user id"""
@@ -70,81 +34,29 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get('%s?user=142141' % self.link)
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
 
     def test_list_handles_search(self):
         """list returns found username changes"""
         self.user.set_username('NewUsername', self.user)
-        self.user.save()
 
         override_acl(self.user, {'can_see_users_name_history': False})
 
         response = self.client.get('%s?user=%s&search=new' % (self.link, self.user.pk))
-        self.assertEqual(response.status_code, 200)
-        
-        username_change = self.user.namechanges.all()[0]
-        self.assertEqual(response.json(), {
-            'page': 1,
-            'pages': 1,
-            'count': 1,
-            'first': None,
-            'previous': None,
-            'next': None,
-            'last': None,
-            'before': 0,
-            'more': 0,
-            'results': [
-                {
-                    'id': username_change.id,
-                    'user': {
-                        'id': self.user.id,
-                        'username': 'NewUsername',
-                        'slug': 'newusername',
-                        'avatars': self.user.avatars,
-                    },
-                    'changed_by': {
-                        'id': self.user.id,
-                        'username': 'NewUsername',
-                        'slug': 'newusername',
-                        'avatars': self.user.avatars,
-                    },
-                    'changed_by_username': 'NewUsername',
-                    'changed_on': serialize_datetime(username_change.changed_on),
-                    'new_username': 'NewUsername',
-                    'old_username': 'TestUser'
-                }
-            ],
-        })
 
-        override_acl(self.user, {'can_see_users_name_history': False})
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, self.user.username)
 
         response = self.client.get('%s?user=%s&search=usernew' % (self.link, self.user.pk))
+
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'page': 1,
-            'pages': 1,
-            'count': 0,
-            'first': None,
-            'previous': None,
-            'next': None,
-            'last': None,
-            'before': 0,
-            'more': 0,
-            'results': [],
-        })
+        self.assertContains(response, '[]')
 
     def test_list_denies_permission(self):
         """list denies permission for other user (or all) if no access"""
         override_acl(self.user, {'can_see_users_name_history': False})
 
         response = self.client.get('%s?user=%s' % (self.link, self.user.pk + 1))
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to see other users name history.",
-        })
+        self.assertContains(response, "don't have permission to", status_code=403)
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You don't have permission to see other users name history.",
-        })
+        self.assertContains(response, "don't have permission to", status_code=403)

+ 23 - 54
misago/users/tests/test_users_api.py

@@ -214,7 +214,7 @@ class RankListTests(AuthenticatedUserTestCase):
             is_tab=True,
         )
 
-        UserModel.objects.create_user(
+        test_user = UserModel.objects.create_user(
             'Visible',
             'visible@te.com',
             'Pass.123',
@@ -223,16 +223,14 @@ class RankListTests(AuthenticatedUserTestCase):
         )
 
         response = self.client.get(self.link % test_rank.pk)
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json()['count'], 0)
+        self.assertNotContains(response, test_user.get_absolute_url())
 
         # api shows disabled accounts to staff
         self.user.is_staff = True
         self.user.save()
 
         response = self.client.get(self.link % test_rank.pk)
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json()['count'], 1)
+        self.assertContains(response, test_user.get_absolute_url())
 
 
 class SearchNamesListTests(AuthenticatedUserTestCase):
@@ -349,7 +347,7 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
                 'subscribe_to_replied_threads': 1,
             }
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.reload_user()
 
@@ -367,7 +365,7 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
                 'subscribe_to_replied_threads': 1,
             }
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.reload_user()
 
@@ -385,7 +383,7 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
                 'subscribe_to_replied_threads': 1,
             }
         )
-        self.assertEqual(response.status_code, 204)
+        self.assertEqual(response.status_code, 200)
 
         self.reload_user()
 
@@ -410,18 +408,12 @@ class UserFollowTests(AuthenticatedUserTestCase):
         self.logout_user()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "This action is not available to guests.",
-        })
+        self.assertContains(response, "action is not available to guests", status_code=403)
 
     def test_follow_myself(self):
         """you can't follow yourself"""
         response = self.client.post('/api/users/%s/follow/' % self.user.pk)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't add yourself to followed.",
-        })
+        self.assertContains(response, "can't add yourself to followed", status_code=403)
 
     def test_cant_follow(self):
         """no permission to follow users"""
@@ -430,10 +422,7 @@ class UserFollowTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't follow other users.",
-        })
+        self.assertContains(response, "can't follow other users", status_code=403)
 
     def test_follow(self):
         """follow and unfollow other user"""
@@ -483,10 +472,7 @@ class UserBanTests(AuthenticatedUserTestCase):
         override_acl(self.user, {'can_see_ban_details': 0})
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't see users bans details.",
-        })
+        self.assertContains(response, "can't see users bans details", status_code=403)
 
     def test_no_ban(self):
         """api returns empty json"""
@@ -494,7 +480,7 @@ class UserBanTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {})
+        self.assertEqual(smart_str(response.content), '{}')
 
     def test_ban_details(self):
         """api returns ban json"""
@@ -508,14 +494,10 @@ class UserBanTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'user_message': {
-                'plain': 'Nope!',
-                'html': '<p>Nope!</p>',
-            },
-            'staff_message': None,
-            'expires_on': None,
-        })
+
+        ban_json = response.json()
+        self.assertEqual(ban_json['user_message']['plain'], 'Nope!')
+        self.assertEqual(ban_json['user_message']['html'], '<p>Nope!</p>')
 
 
 class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
@@ -621,9 +603,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete users.",
-        })
+        self.assertContains(response, "can't delete users", status_code=403)
 
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
@@ -639,9 +619,8 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete users that made more than 5 posts.",
-        })
+        self.assertContains(response, "can't delete users", status_code=403)
+        self.assertContains(response, "made more than 5 posts", status_code=403)
 
     def test_delete_too_old_member(self):
         """raises 403 error when user is too old"""
@@ -657,9 +636,8 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete users that are members for more than 5 days.",
-        })
+        self.assertContains(response, "can't delete users", status_code=403)
+        self.assertContains(response, "members for more than 5 days", status_code=403)
 
     def test_delete_self(self):
         """raises 403 error when attempting to delete oneself"""
@@ -671,10 +649,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         )
 
         response = self.client.post('/api/users/%s/delete/' % self.user.pk)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete your account.",
-        })
+        self.assertContains(response, "can't delete your account", status_code=403)
 
     def test_delete_admin(self):
         """raises 403 error when attempting to delete admin"""
@@ -689,10 +664,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete administrators.",
-        })
+        self.assertContains(response, "can't delete administrators", status_code=403)
 
     def test_delete_superadmin(self):
         """raises 403 error when attempting to delete superadmin"""
@@ -707,10 +679,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertEqual(response.json(), {
-            'detail': "You can't delete administrators.",
-        })
+        self.assertContains(response, "can't delete administrators", status_code=403)
 
     def test_delete_with_content(self):
         """returns 200 and deletes user with content"""

+ 11 - 2
misago/users/testutils.py

@@ -10,6 +10,7 @@ UserModel = get_user_model()
 
 class UserTestCase(MisagoTestCase):
     USER_PASSWORD = "Pass.123"
+    USER_IP = '127.0.0.1'
 
     def setUp(self):
         super(UserTestCase, self).setUp()
@@ -22,12 +23,20 @@ class UserTestCase(MisagoTestCase):
         return AnonymousUser()
 
     def get_authenticated_user(self):
-        return UserModel.objects.create_user("TestUser", "test@user.com", self.USER_PASSWORD)
+        return UserModel.objects.create_user(
+            "TestUser",
+            "test@user.com",
+            self.USER_PASSWORD,
+            joined_from_ip=self.USER_IP,
+        )
 
     def get_superuser(self):
-        return UserModel.objects.create_superuser(
+        user = UserModel.objects.create_superuser(
             "TestSuperUser", "test@superuser.com", self.USER_PASSWORD
         )
+        user.joined_from_ip = self.USER_IP
+        user.save()
+        return user
 
     def login_user(self, user, password=None):
         self.client.force_login(user)

+ 2 - 0
misago/users/urls/__init__.py

@@ -39,6 +39,8 @@ urlpatterns += [
         options.confirm_password_change,
         name='options-confirm-password-change'
     ),
+    url(r'^options/dowload-data/$', options.index, name='usercp-download-data'),
+    url(r'^options/delete-account/$', options.index, name='usercp-delete-account'),
 ]
 
 urlpatterns += [

+ 7 - 3
misago/users/urls/api.py

@@ -1,6 +1,6 @@
 from django.conf.urls import url
 
-from misago.api.router import MisagoApiRouter
+from misago.core.apirouter import MisagoApiRouter
 from misago.users.api import auth, captcha, mention
 from misago.users.api.ranks import RanksViewSet
 from misago.users.api.usernamechanges import UsernameChangesViewSet
@@ -9,10 +9,14 @@ from misago.users.api.users import UserViewSet
 
 urlpatterns = [
     url(r'^auth/$', auth.gateway, name='auth'),
-    url(r'^auth/requirements/$', auth.get_requirements, name='auth-requirements'),
+    url(r'^auth/criteria/$', auth.get_criteria, name='auth-criteria'),
     url(r'^auth/send-activation/$', auth.send_activation, name='send-activation'),
     url(r'^auth/send-password-form/$', auth.send_password_form, name='send-password-form'),
-    url(r'^auth/change-password/(?P<pk>\d+)/', auth.change_forgotten_password, name='change-forgotten-password'),
+    url(
+        r'^auth/change-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$',
+        auth.change_forgotten_password,
+        name='change-forgotten-password'
+    ),
     url(r'^captcha-question/$', captcha.question, name='captcha-question'),
     url(r'^mention/$', mention.mention_suggestions, name='mention-suggestions'),
 ]

+ 4 - 0
misago/users/views/activation.py

@@ -1,5 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.shortcuts import get_object_or_404, render
+from django.urls import reverse
 from django.utils.translation import ugettext as _
 
 from misago.core.exceptions import Banned
@@ -22,6 +23,9 @@ def activation_view(f):
 
 @activation_view
 def request_activation(request):
+    request.frontend_context.update({
+        'SEND_ACTIVATION_API': reverse('misago:api:send-activation'),
+    })
     return render(request, 'misago/activation/request.html')
 
 

+ 1 - 1
misago/users/views/admin/bans.py

@@ -2,7 +2,7 @@ from django.contrib import messages
 from django.utils.translation import ugettext_lazy as _
 
 from misago.admin.views import generic
-from misago.users.forms import BanForm, SearchBansForm
+from misago.users.forms.admin import BanForm, SearchBansForm
 from misago.users.models import Ban
 
 

+ 68 - 0
misago/users/views/admin/datadownloads.py

@@ -0,0 +1,68 @@
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
+
+from misago.admin.views import generic
+from misago.users.datadownloads import (
+    expire_user_data_download, request_user_data_download, user_has_data_download_request)
+from misago.users.forms.admin import RequestDataDownloadsForm, SearchDataDownloadsForm
+from misago.users.models import DataDownload
+
+
+class DataDownloadAdmin(generic.AdminBaseMixin):
+    root_link = 'misago:admin:users:data-downloads:index'
+    templates_dir = 'misago/admin/datadownloads'
+    model = DataDownload
+
+
+class DataDownloadsList(DataDownloadAdmin, generic.ListView):
+    items_per_page = 30
+    ordering = [
+        ('-id', _("From newest")),
+        ('id', _("From oldest")),
+    ]
+    selection_label = _('With data downloads: 0')
+    empty_selection_label = _('Select data downloads')
+    mass_actions = [
+        {
+            'action': 'expire',
+            'name': _("Expire downloads"),
+            'icon': 'fa fa-ban',
+            'confirmation': _("Are you sure you want to set selected data downloads as expired?"),
+        },
+        {
+            'action': 'delete',
+            'name': _("Delete downloads"),
+            'icon': 'fa fa-times-circle',
+            'confirmation': _("Are you sure you want to delete selected data downloads?"),
+        },
+    ]
+
+    def get_queryset(self):
+        qs = super(DataDownloadsList, self).get_queryset()
+        return qs.select_related('user', 'requester')
+        
+    def get_search_form(self, request):
+        return SearchDataDownloadsForm
+
+    def action_expire(self, request, data_downloads):
+        for data_download in data_downloads:
+            expire_user_data_download(data_download)
+
+        messages.success(request, _("Selected data downloads have been set as expired."))
+
+    def action_delete(self, request, data_downloads):
+        for data_download in data_downloads:
+            data_download.delete()
+
+        messages.success(request, _("Selected data downloads have been deleted."))
+
+
+class RequestDataDownloads(DataDownloadAdmin, generic.FormView):
+    form = RequestDataDownloadsForm
+
+    def handle_form(self, form, request):
+        for user in form.cleaned_data['users']:
+            if not user_has_data_download_request(user):
+                request_user_data_download(user, requester=request.user)
+
+        messages.success(request, _("Data downloads have been requested for specified users."))

+ 1 - 1
misago/users/views/admin/ranks.py

@@ -4,7 +4,7 @@ from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 
 from misago.admin.views import generic
-from misago.users.forms import RankForm
+from misago.users.forms.admin import RankForm
 from misago.users.models import Rank
 
 

+ 24 - 9
misago/users/views/admin/users.py

@@ -13,7 +13,8 @@ from misago.core.mail import mail_users
 from misago.core.pgutils import chunk_queryset
 from misago.threads.models import Thread
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
-from misago.users.forms import (
+from misago.users.datadownloads import request_user_data_download, user_has_data_download_request
+from misago.users.forms.admin import (
     BanUsersForm, EditUserForm, EditUserFormFactory, NewUserForm, SearchUsersForm)
 from misago.users.models import Ban
 from misago.users.profilefields import profilefields
@@ -73,6 +74,11 @@ class UsersList(UserAdmin, generic.ListView):
             'icon': 'fa fa-lock',
         },
         {
+            'action': 'request_data_download',
+            'name': _("Request data download"),
+            'icon': 'fa fa-download',
+        },
+        {
             'action': 'delete_accounts',
             'name': _("Delete accounts"),
             'icon': 'fa fa-times-circle',
@@ -114,7 +120,7 @@ class UsersList(UserAdmin, generic.ListView):
             subject = _("Your account on %(forum_name)s forums has been activated")
             mail_subject = subject % {'forum_name': settings.forum_name}
 
-            mail_users(request, inactive_users, mail_subject, 'misago/emails/activation/by_admin')
+            mail_users(inactive_users, mail_subject, 'misago/emails/activation/by_admin')
 
             messages.success(request, _("Selected users accounts have been activated."))
 
@@ -126,9 +132,9 @@ class UsersList(UserAdmin, generic.ListView):
                 mesage = message % {'user': user.username}
                 raise generic.MassActionError(mesage)
 
-        form = BanUsersForm()
+        form = BanUsersForm(users=users)
         if 'finalize' in request.POST:
-            form = BanUsersForm(request.POST)
+            form = BanUsersForm(request.POST, users=users)
             if form.is_valid():
                 cleaned_data = form.cleaned_data
                 banned_values = []
@@ -141,6 +147,8 @@ class UsersList(UserAdmin, generic.ListView):
 
                 for user in users:
                     for ban in cleaned_data['ban_type']:
+                        banned_value = None
+
                         if ban == 'usernames':
                             check_type = Ban.USERNAME
                             banned_value = user.username.lower()
@@ -155,11 +163,11 @@ class UsersList(UserAdmin, generic.ListView):
                             at_pos = banned_value.find('@')
                             banned_value = '*%s' % banned_value[at_pos:]
 
-                        if ban == 'ip':
+                        if ban == 'ip' and user.joined_from_ip:
                             check_type = Ban.IP
                             banned_value = user.joined_from_ip
 
-                        if ban in ('ip_first', 'ip_two'):
+                        if ban in ('ip_first', 'ip_two') and user.joined_from_ip:
                             check_type = Ban.IP
 
                             if ':' in user.joined_from_ip:
@@ -174,7 +182,7 @@ class UsersList(UserAdmin, generic.ListView):
                                 formats = (bits[0], ip_separator, bits[1], ip_separator)
                             banned_value = '%s*' % (''.join(formats))
 
-                        if banned_value not in banned_values:
+                        if banned_value and banned_value not in banned_values:
                             ban_kwargs.update({
                                 'check_type': check_type,
                                 'banned_value': banned_value,
@@ -195,6 +203,14 @@ class UsersList(UserAdmin, generic.ListView):
             }
         )
 
+    def action_request_data_download(self, request, users):
+        for user in users:
+            if not user_has_data_download_request(user):
+                request_user_data_download(user, requester=request.user)
+
+        messages.success(
+            request, _("Data download requests have been placed for selected users."))
+
     def action_delete_accounts(self, request, users):
         for user in users:
             if user == request.user:
@@ -206,8 +222,7 @@ class UsersList(UserAdmin, generic.ListView):
         for user in users:
             user.delete()
 
-        message = _("Selected users have been deleted.")
-        messages.success(request, message)
+        messages.success(request, _("Selected users have been deleted."))
 
     def action_delete_all(self, request, users):
         for user in users:

+ 10 - 6
misago/users/views/forgottenpassword.py

@@ -19,6 +19,9 @@ def reset_view(f):
 
 @reset_view
 def request_reset(request):
+    request.frontend_context.update({
+        'SEND_PASSWORD_RESET_API': reverse('misago:api:send-password-form'),
+    })
     return render(request, 'misago/forgottenpassword/request.html')
 
 
@@ -28,7 +31,7 @@ class ResetError(Exception):
 
 @reset_view
 def reset_password_form(request, pk, token):
-    requesting_user = get_object_or_404(get_user_model(), pk=pk, is_active=True)
+    requesting_user = get_object_or_404(get_user_model(), pk=pk)
 
     try:
         if (request.user.is_authenticated and request.user.id != requesting_user.id):
@@ -49,11 +52,12 @@ def reset_password_form(request, pk, token):
             }, status=400
         )
 
-    request.frontend_context['store'].update({
-        'forgotten_password': {
-            'id': pk,
+    api_url = reverse(
+        'misago:api:change-forgotten-password', kwargs={
+            'pk': pk,
             'token': token,
-        },
-    })
+        }
+    )
 
+    request.frontend_context['CHANGE_PASSWORD_API'] = api_url
     return render(request, 'misago/forgottenpassword/form.html')

+ 2 - 2
misago/users/views/profile.py

@@ -196,6 +196,6 @@ class UserBanView(ProfileView):
 
 UserProfileSerializer = UserSerializer.subset_fields(
     'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars', 'is_avatar_locked',
-    'signature', 'is_signature_locked', 'followers', 'following', 'threads', 'posts',
-    'is_followed', 'is_blocked', 'real_name', 'status',
+    'signature', 'is_signature_locked', 'followers', 'following', 'threads', 'posts', 'acl',
+    'is_followed', 'is_blocked', 'real_name', 'status', 'api', 'url'
 )

+ 1 - 0
requirements.txt

@@ -13,6 +13,7 @@ path.py~=10.3.1
 pillow~=4.1.1
 psycopg2-binary~=2.7.1
 pytz
+pyyaml~=3.13
 requests<3
 misago-social-auth-app-django~=2.1.0
 unidecode~=0.4.20

+ 3 - 0
runtests.py

@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 import os
+import pwd
 import shutil
 import sys
 
@@ -82,6 +83,8 @@ PASSWORD_HASHERS = (
     'django.contrib.auth.hashers.MD5PasswordHasher',
 )
 
+# Default misago address to test address
+MISAGO_ADDRESS = 'http://testserver/'
 
 # Use english search config
 MISAGO_SEARCH_CONFIG = 'english'

Некоторые файлы не были показаны из-за большого количества измененных файлов