From 0c34e5d54c20168d1f7022fd1a236a807d7ef661 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 20 Jan 2019 20:58:02 +0100 Subject: [PATCH 01/12] Add utility for cursor-based pagination --- misago/core/cursorpaginator.py | 43 ++++++++++++ misago/core/tests/test_cursor_paginator.py | 79 ++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 misago/core/cursorpaginator.py create mode 100644 misago/core/tests/test_cursor_paginator.py diff --git a/misago/core/cursorpaginator.py b/misago/core/cursorpaginator.py new file mode 100644 index 0000000000..5aafc86b4a --- /dev/null +++ b/misago/core/cursorpaginator.py @@ -0,0 +1,43 @@ +from django.core.paginator import EmptyPage, InvalidPage + + +class CursorPaginator: + def __init__(self, queryset, order_by, per_page): + self.queryset = queryset + self.order_by = order_by + self.per_page = int(per_page) + + def get_page(self, start=0): + if start < 0: + raise InvalidPage() + + object_list = self._get_slice(start) + if start and not object_list: + raise EmptyPage() + + next_cursor = None + if len(object_list) > self.per_page: + next_slice_first_item = object_list.pop(-1) + next_cursor = getattr(next_slice_first_item, self.order_by) + + return Page(start, object_list, next_cursor) + + def _get_slice(self, start): + page_len = self.per_page + 1 + if start: + filter_name = "%s__gte" % self.order_by + return self.queryset.filter(**{filter_name: start})[:page_len] + return self.queryset[:page_len] + + +class Page: + def __init__(self, start, object_list, next_): + self.start = start or None + self.object_list = object_list + self.next = next_ + + def __len__(self): + return len(self.object_list) + + def has_next(self): + return bool(self.next) diff --git a/misago/core/tests/test_cursor_paginator.py b/misago/core/tests/test_cursor_paginator.py new file mode 100644 index 0000000000..9180a0fcfa --- /dev/null +++ b/misago/core/tests/test_cursor_paginator.py @@ -0,0 +1,79 @@ +import pytest + +from ..cursorpaginator import CursorPaginator, EmptyPage, InvalidPage + + +@pytest.fixture +def mock_objects(mocker): + return [mocker.Mock(post=i) for i in range(1, 12)] + + +@pytest.fixture +def mock_queryset(mocker, mock_objects): + return mocker.Mock( + filter=mocker.Mock(return_value=mock_objects) + ) + + +def test_paginator_returns_first_page(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert paginator.get_page() + + +def test_first_page_has_no_start(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert paginator.get_page().start is None + + +def test_first_page_has_correct_length(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert len(paginator.get_page().object_list) == 6 + + +def test_first_page_has_correct_items(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert paginator.get_page().object_list == mock_objects[:6] + + +def test_page_has_next_attr_pointing_to_first_item_of_next_page(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert paginator.get_page().next == 7 + + +def test_page_can_be_tested_to_see_if_next_page_exists(mock_objects): + paginator = CursorPaginator(mock_objects, "post", 6) + assert paginator.get_page().has_next() + + +def test_paginator_returns_empty_first_page_without_errors(): + paginator = CursorPaginator([], "post", 6) + assert paginator.get_page().object_list == [] + + +def test_paginator_returns_page_starting_at_requested_address(mock_queryset): + paginator = CursorPaginator(mock_queryset, "post", 6) + assert paginator.get_page(7) + + +def test_requesting_next_page_filters_queryset_using_filter_name(mock_queryset): + paginator = CursorPaginator(mock_queryset, "post", 6) + paginator.get_page(7) + mock_queryset.filter.assert_called_once_with(post__gte=7) + + +def test_requesting_next_page_limits_queryset_to_specified_length(mock_queryset): + paginator = CursorPaginator(mock_queryset, "post", 6) + assert len(paginator.get_page(7).object_list) == 6 + + +def test_paginator_raises_empty_page_error_if_nth_page_is_empty(mocker): + queryset = mocker.Mock(filter=lambda **_: []) + paginator = CursorPaginator(queryset, "post", 6) + with pytest.raises(EmptyPage): + paginator.get_page(20) + + +def test_paginator_raises_invalid_page_error_if_starting_position_is_negative(): + paginator = CursorPaginator(None, None, 0) + with pytest.raises(InvalidPage): + paginator.get_page(-1) From 4b47c45ef4c676bbea823f05414bdfdc5053cf12 Mon Sep 17 00:00:00 2001 From: rafalp Date: Thu, 31 Jan 2019 21:28:46 +0100 Subject: [PATCH 02/12] Format cursor paginator tests with black --- misago/core/tests/test_cursor_paginator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/misago/core/tests/test_cursor_paginator.py b/misago/core/tests/test_cursor_paginator.py index 9180a0fcfa..534b0bd681 100644 --- a/misago/core/tests/test_cursor_paginator.py +++ b/misago/core/tests/test_cursor_paginator.py @@ -10,9 +10,7 @@ def mock_objects(mocker): @pytest.fixture def mock_queryset(mocker, mock_objects): - return mocker.Mock( - filter=mocker.Mock(return_value=mock_objects) - ) + return mocker.Mock(filter=mocker.Mock(return_value=mock_objects)) def test_paginator_returns_first_page(mock_objects): From b4e1c31f687aef07d2d25ccb3bbaeb00030b8075 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 2 Feb 2019 18:25:52 +0100 Subject: [PATCH 03/12] Add fakebigdata shortcut --- dev | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dev b/dev index 2d84bb7e5a..0aeb7fd1b1 100755 --- a/dev +++ b/dev @@ -91,6 +91,7 @@ intro() { echo " ${BOLD}psql${NORMAL} runs psql connected to development database." echo " ${BOLD}pyfmt${NORMAL} runs isort + black on python code." echo " ${BOLD}fakedata${NORMAL} populates database with testing data." + echo " ${BOLD}fakebigdata${NORMAL} populates database with LARGE amount of testing data." echo } @@ -271,6 +272,13 @@ create_fake_data() { docker-compose run --rm misago python manage.py createfakehistory 600 } +# Shortcut for creating big dev forum +create_fake_bigdata() { + docker-compose run --rm misago python manage.py createfakecategories 48 + docker-compose run --rm misago python manage.py createfakecategories 24 1 + docker-compose run --rm misago python manage.py createfakehistory 2190 120 +} + # Command dispatcher if [[ $1 ]]; then if [[ $1 = "init" ]]; then @@ -319,6 +327,8 @@ if [[ $1 ]]; then black devproject misago elif [[ $1 = "fakedata" ]]; then create_fake_data + elif [[ $1 = "fakebigdata" ]]; then + create_fake_bigdata else invalid_argument $1 fi From a31b11cfb70b74b9221aa74951a2763a4c7ea542 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 3 Feb 2019 00:15:52 +0100 Subject: [PATCH 04/12] Basic implementation for cursor based pagination on threads list --- misago/core/cursorpaginator.py | 14 +++++++++----- misago/threads/viewmodels/threads.py | 14 +++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/misago/core/cursorpaginator.py b/misago/core/cursorpaginator.py index 5aafc86b4a..02d8469a8a 100644 --- a/misago/core/cursorpaginator.py +++ b/misago/core/cursorpaginator.py @@ -7,32 +7,36 @@ def __init__(self, queryset, order_by, per_page): self.order_by = order_by self.per_page = int(per_page) - def get_page(self, start=0): + def get(self, start=0): if start < 0: raise InvalidPage() - object_list = self._get_slice(start) + object_list = list(self._get_slice(start)) if start and not object_list: raise EmptyPage() next_cursor = None if len(object_list) > self.per_page: next_slice_first_item = object_list.pop(-1) - next_cursor = getattr(next_slice_first_item, self.order_by) + attr_name = self.order_by.lstrip("-") + next_cursor = getattr(next_slice_first_item, attr_name) return Page(start, object_list, next_cursor) def _get_slice(self, start): page_len = self.per_page + 1 if start: - filter_name = "%s__gte" % self.order_by + if self.order_by.startswith("-"): + filter_name = "%s__lte" % self.order_by[1:] + else: + filter_name = "%s__gte" % self.order_by return self.queryset.filter(**{filter_name: start})[:page_len] return self.queryset[:page_len] class Page: def __init__(self, start, object_list, next_): - self.start = start or None + self.start = start or 0 self.object_list = object_list self.next = next_ diff --git a/misago/threads/viewmodels/threads.py b/misago/threads/viewmodels/threads.py index 97326938f6..8032a620a8 100644 --- a/misago/threads/viewmodels/threads.py +++ b/misago/threads/viewmodels/threads.py @@ -6,6 +6,7 @@ from ...acl.objectacl import add_acl_to_obj from ...conf import settings +from ...core.cursorpaginator import CursorPaginator from ...core.shortcuts import paginate, pagination_dict from ...readtracker import threadstracker from ...readtracker.dates import get_cutoff_date @@ -59,15 +60,14 @@ def __init__(self, request, category, list_type, page): base_queryset, category_model, threads_categories ) - list_page = paginate( + paginator = CursorPaginator( threads_queryset, - page, - settings.MISAGO_THREADS_PER_PAGE, - settings.MISAGO_THREADS_TAIL, + "-last_post_id", + settings.MISAGO_THREADS_PER_PAGE ) - paginator = pagination_dict(list_page) + list_page = paginator.get(0) - if list_page.number > 1: + if list_page.start: threads = list(list_page.object_list) else: pinned_threads = list( @@ -136,7 +136,7 @@ def get_frontend_context(self): } } - context["THREADS"].update(self.paginator) + #context["THREADS"].update(self.paginator) return context def get_template_context(self): From 66364d4bdc73371d266ca4cd07b1306db72c5753 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 3 Feb 2019 01:52:58 +0100 Subject: [PATCH 05/12] Move threads list to cursor pagination --- frontend/src/components/threads/route.js | 58 +++++++------------ misago/core/cursorpaginator.py | 19 ++++-- misago/static/misago/js/misago.js | 50 ++++++++-------- misago/static/misago/js/misago.js.map | 2 +- misago/templates/misago/threadslist/base.html | 44 ++++---------- misago/threads/api/threadendpoints/list.py | 12 ++-- misago/threads/viewmodels/threads.py | 15 ++--- misago/threads/views/list.py | 8 +-- 8 files changed, 84 insertions(+), 124 deletions(-) diff --git a/frontend/src/components/threads/route.js b/frontend/src/components/threads/route.js index 642fee3c50..16a613062d 100644 --- a/frontend/src/components/threads/route.js +++ b/frontend/src/components/threads/route.js @@ -44,12 +44,8 @@ export default class extends WithDropdown { dropdown: false, subcategories: [], - - count: 0, - more: 0, - - page: 1, - pages: 1 + + next: 0, } let category = this.getCategory() @@ -72,14 +68,8 @@ export default class extends WithDropdown { initWithPreloadedData(category, data) { this.state = Object.assign(this.state, { moderation: getModerationActions(data.results), - subcategories: data.subcategories, - - count: data.count, - more: data.more, - - page: data.page, - pages: data.pages + next: data.next }) this.startPolling(category) @@ -89,14 +79,14 @@ export default class extends WithDropdown { this.loadThreads(category) } - loadThreads(category, page = 1) { + loadThreads(category, next = 0) { ajax .get( this.props.options.api, { category: category, list: this.props.route.list.type, - page: page || 1 + start: next || 0 }, "threads" ) @@ -107,7 +97,7 @@ export default class extends WithDropdown { return } - if (page === 1) { + if (next === 0) { store.dispatch(hydrate(data.results)) } else { store.dispatch(append(data.results, this.getSorting())) @@ -121,11 +111,7 @@ export default class extends WithDropdown { subcategories: data.subcategories, - count: data.count, - more: data.more, - - page: data.page, - pages: data.pages + next: data.next, }) this.startPolling(category) @@ -207,7 +193,7 @@ export default class extends WithDropdown { isBusy: true }) - this.loadThreads(this.getCategory(), this.state.page + 1) + this.loadThreads(this.getCategory(), this.state.next) } pollResponse = data => { @@ -255,21 +241,19 @@ export default class extends WithDropdown { } getMoreButton() { - if (this.state.more) { - return ( -
- -
- ) - } else { - return null - } + if (!this.state.next) return null + + return ( +
+ +
+ ) } getClassName() { diff --git a/misago/core/cursorpaginator.py b/misago/core/cursorpaginator.py index 02d8469a8a..21c202e8de 100644 --- a/misago/core/cursorpaginator.py +++ b/misago/core/cursorpaginator.py @@ -4,10 +4,16 @@ class CursorPaginator: def __init__(self, queryset, order_by, per_page): self.queryset = queryset - self.order_by = order_by self.per_page = int(per_page) - def get(self, start=0): + if order_by.startswith("-"): + self.order_by = order_by[1:] + self.desc = True + else: + self.order_by = order_by + self.desc = False + + def get_page(self, start=0): if start < 0: raise InvalidPage() @@ -18,18 +24,19 @@ def get(self, start=0): next_cursor = None if len(object_list) > self.per_page: next_slice_first_item = object_list.pop(-1) - attr_name = self.order_by.lstrip("-") - next_cursor = getattr(next_slice_first_item, attr_name) + next_cursor = getattr(next_slice_first_item, self.order_by) return Page(start, object_list, next_cursor) def _get_slice(self, start): page_len = self.per_page + 1 if start: - if self.order_by.startswith("-"): - filter_name = "%s__lte" % self.order_by[1:] + print(start) + if self.desc: + filter_name = "%s__lte" % self.order_by else: filter_name = "%s__gte" % self.order_by + print({filter_name: start}) return self.queryset.filter(**{filter_name: start})[:page_len] return self.queryset[:page_len] diff --git a/misago/static/misago/js/misago.js b/misago/static/misago/js/misago.js index 24a3c9c32d..4910caf558 100644 --- a/misago/static/misago/js/misago.js +++ b/misago/static/misago/js/misago.js @@ -1,26 +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%(agreement)s',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)return null;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&&(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;an.height){var r=n.width*a,o=(r-e.getAvatarSize())/-2;t.cropit("offset",{x:o,y:0})}else if(n.widththis.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;a0?"!["+n+"]("+a+")":"!("+a+")")}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t0?"["+n+"]("+a+")":a)))}Object.defineProperty(a,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t%(relative)s',x='%(user)s',P='%(user)s',j=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u%(name)s",p=function(e){function t(){var e,a,n,l;r(this,t);for(var s=arguments.length,i=Array(s),u=0;u%(name)s"},{"../../../../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${username}',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;a0});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;n0){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