##// END OF EJS Templates
Subscribe/unsubscribe thread by ajax
neko259 -
r2089:676b3885 default
parent child Browse files
Show More
@@ -1,143 +1,160 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var REPLY_TO_MSG = '.reply-to-message';
26 var REPLY_TO_MSG = '.reply-to-message';
27 var REPLY_TO_MSG_ID = '#reply-to-message-id';
27 var REPLY_TO_MSG_ID = '#reply-to-message-id';
28
28
29 var $html = $("html, body");
29 var $html = $("html, body");
30 var $quoteButton = $("#quote-button");
30 var $quoteButton = $("#quote-button");
31
31
32 function moveCaretToEnd(el) {
32 function moveCaretToEnd(el) {
33 var newPos = el.val().length;
33 var newPos = el.val().length;
34 el[0].setSelectionRange(newPos, newPos);
34 el[0].setSelectionRange(newPos, newPos);
35 }
35 }
36
36
37
37
38 function showFormAfter(blockToInsertAfter) {
38 function showFormAfter(blockToInsertAfter) {
39 var form = getForm();
39 var form = getForm();
40 form.insertAfter(blockToInsertAfter);
40 form.insertAfter(blockToInsertAfter);
41
41
42 form.show();
42 form.show();
43 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
43 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
44 $(REPLY_TO_MSG).show();
44 $(REPLY_TO_MSG).show();
45 }
45 }
46
46
47 function addQuickReply(postId) {
47 function addQuickReply(postId) {
48 var blockToInsert = null;
48 var blockToInsert = null;
49 var textAreaJq = getPostTextarea();
49 var textAreaJq = getPostTextarea();
50 var postLinkRaw = '[post]' + postId + '[/post]'
50 var postLinkRaw = '[post]' + postId + '[/post]'
51 var textToAdd = '';
51 var textToAdd = '';
52
52
53 if (postId != null) {
53 if (postId != null) {
54 var post = $('#' + postId);
54 var post = $('#' + postId);
55
55
56 // If this is not OP, add reflink to the post. If there already is
56 // If this is not OP, add reflink to the post. If there already is
57 // the same reflink, don't add it again.
57 // the same reflink, don't add it again.
58 var postText = textAreaJq.val();
58 var postText = textAreaJq.val();
59 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
59 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
60 // Insert line break if none is present.
60 // Insert line break if none is present.
61 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
61 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
62 textToAdd += '\n';
62 textToAdd += '\n';
63 }
63 }
64 textToAdd += postLinkRaw + '\n';
64 textToAdd += postLinkRaw + '\n';
65 }
65 }
66
66
67 textAreaJq.val(textAreaJq.val()+ textToAdd);
67 textAreaJq.val(textAreaJq.val()+ textToAdd);
68 blockToInsert = post;
68 blockToInsert = post;
69 } else {
69 } else {
70 blockToInsert = $('.thread');
70 blockToInsert = $('.thread');
71 }
71 }
72 showFormAfter(blockToInsert);
72 showFormAfter(blockToInsert);
73
73
74 textAreaJq.focus();
74 textAreaJq.focus();
75
75
76 moveCaretToEnd(textAreaJq);
76 moveCaretToEnd(textAreaJq);
77 }
77 }
78
78
79 function addQuickQuote() {
79 function addQuickQuote() {
80 var textAreaJq = getPostTextarea();
80 var textAreaJq = getPostTextarea();
81
81
82 var postId = $quoteButton.attr('data-post-id');
82 var postId = $quoteButton.attr('data-post-id');
83 if (postId != null) {
83 if (postId != null) {
84 addQuickReply(postId);
84 addQuickReply(postId);
85 }
85 }
86
86
87 var textToAdd = '';
87 var textToAdd = '';
88 var selection = window.getSelection().toString();
88 var selection = window.getSelection().toString();
89 if (selection.length == 0) {
89 if (selection.length == 0) {
90 selection = $quoteButton.attr('data-text');
90 selection = $quoteButton.attr('data-text');
91 }
91 }
92 if (selection.length > 0) {
92 if (selection.length > 0) {
93 textToAdd += '[quote]' + selection + '[/quote]\n';
93 textToAdd += '[quote]' + selection + '[/quote]\n';
94 }
94 }
95
95
96 textAreaJq.val(textAreaJq.val() + textToAdd);
96 textAreaJq.val(textAreaJq.val() + textToAdd);
97
97
98 textAreaJq.focus();
98 textAreaJq.focus();
99
99
100 moveCaretToEnd(textAreaJq);
100 moveCaretToEnd(textAreaJq);
101
101
102 $quoteButton.hide();
102 $quoteButton.hide();
103 }
103 }
104
104
105 function scrollToBottom() {
105 function scrollToBottom() {
106 $html.animate({scrollTop: $html.height()}, "fast");
106 $html.animate({scrollTop: $html.height()}, "fast");
107 }
107 }
108
108
109 function showQuoteButton() {
109 function showQuoteButton() {
110 var selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
110 var selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
111 if (selection.width > 0) {
111 if (selection.width > 0) {
112 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
112 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
113 $quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
113 $quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
114 $quoteButton.show();
114 $quoteButton.show();
115
115
116 var text = window.getSelection().toString();
116 var text = window.getSelection().toString();
117 $quoteButton.attr('data-text', text);
117 $quoteButton.attr('data-text', text);
118
118
119 var rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
119 var rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
120 var element = $(document.elementFromPoint(rect.x, rect.y));
120 var element = $(document.elementFromPoint(rect.x, rect.y));
121 var postId = null;
121 var postId = null;
122 if (element.hasClass('post')) {
122 if (element.hasClass('post')) {
123 postId = element.attr('id');
123 postId = element.attr('id');
124 } else {
124 } else {
125 var postParent = element.parents('.post');
125 var postParent = element.parents('.post');
126 if (postParent.length > 0) {
126 if (postParent.length > 0) {
127 postId = postParent.attr('id');
127 postId = postParent.attr('id');
128 }
128 }
129 }
129 }
130 $quoteButton.attr('data-post-id', postId);
130 $quoteButton.attr('data-post-id', postId);
131 } else {
131 } else {
132 $quoteButton.hide();
132 $quoteButton.hide();
133 }
133 }
134 }
134 }
135
135
136 function attachAjaxToForms() {
137 var forms = $('form.ajax-form');
138 forms.each(function() {
139 var form = $(this);
140 var options = {
141 success: function(response, statusText, xhr, f) {
142 afterForm(form, $.parseJSON(response));
143 },
144 url: form.attr('action') + '?ajax'
145 };
146
147 form.ajaxForm(options);
148 });
149 }
150
136 $(document).ready(function() {
151 $(document).ready(function() {
137 $('body').on('mouseup', function() {
152 $('body').on('mouseup', function() {
138 showQuoteButton();
153 showQuoteButton();
139 });
154 });
140 $("#quote-button").click(function() {
155 $("#quote-button").click(function() {
141 addQuickQuote();
156 addQuickQuote();
142 })
157 })
158
159 attachAjaxToForms();
143 });
160 });
@@ -1,52 +1,73 b''
1 {% extends "boards/thread.html" %}
1 {% extends "boards/thread.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block thread_content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <button id="quote-button">{% trans 'Quote' %}</button>
12 <button id="quote-button">{% trans 'Quote' %}</button>
13
13
14 <div class="tag_info">
14 <div class="tag_info">
15 <h2>
15 <h2>
16 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
16 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form ajax-form">
17 {% csrf_token %}
17 {% csrf_token %}
18 {% if is_favorite %}
18 {% if is_favorite %}
19 <button id="thread-fav-button" name="method" value="unsubscribe" class="fav">β˜…</button>
19 <input type="hidden" name="method" value="unsubscribe" />
20 <button id="thread-fav-button" class="fav">β˜…</button>
20 {% else %}
21 {% else %}
21 <button id="thread-fav-button" name="method" value="subscribe" class="not_fav">β˜…</button>
22 <input type="hidden" name="method" value="subscribe" />
23 <button id="thread-fav-button" class="not_fav">β˜…</button>
22 {% endif %}
24 {% endif %}
23 </form>
25 </form>
24 {{ opening_post.get_title_or_text }}
26 {{ opening_post.get_title_or_text }}
25 </h2>
27 </h2>
26 </div>
28 </div>
27
29
30 <script>
31 function afterForm(form, data) {
32 if (data.status == 'success') {
33 var button = form.children('button');
34 var newMethod;
35 if (data.fav == 'true') {
36 button.addClass('fav');
37 button.removeClass('not_fav');
38 newMethod = 'unsubscribe';
39 } else {
40 button.addClass('not_fav');
41 button.removeClass('fav');
42 newMethod = 'subscribe';
43 }
44 form.children('input[name=method]').attr('value', newMethod);
45 }
46 }
47 </script>
48
28 {% if bumpable and thread.has_post_limit %}
49 {% if bumpable and thread.has_post_limit %}
29 <div class="bar-bg">
50 <div class="bar-bg">
30 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
51 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
31 </div>
52 </div>
32 <div class="bar-text">
53 <div class="bar-text">
33 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
54 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
34 </div>
55 </div>
35 </div>
56 </div>
36 {% endif %}
57 {% endif %}
37
58
38 <div class="thread">
59 <div class="thread">
39 {% for post in thread.get_viewable_replies %}
60 {% for post in thread.get_viewable_replies %}
40 {% post_view post reply_link=True thread=thread %}
61 {% post_view post reply_link=True thread=thread %}
41 {% endfor %}
62 {% endfor %}
42 </div>
63 </div>
43
64
44 {% if not thread.is_archived %}
65 {% if not thread.is_archived %}
45 {% form_view form=form new_thread=False opening_post_id=opening_post.id %}
66 {% form_view form=form new_thread=False opening_post_id=opening_post.id %}
46
67
47 <script src="{% static 'js/thread.js' %}"></script>
68 <script src="{% static 'js/thread.js' %}"></script>
48 <script src="{% static 'js/thread_update.js' %}"></script>
69 <script src="{% static 'js/thread_update.js' %}"></script>
49 {% endif %}
70 {% endif %}
50
71
51 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
72 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
52 {% endblock %}
73 {% endblock %}
@@ -1,120 +1,135 b''
1 import json
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.core.exceptions import ObjectDoesNotExist
2 from django.http import Http404
3 from django.http import Http404, HttpResponse
3 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.urls import reverse
5 from django.urls import reverse
5 from django.utils.decorators import method_decorator
6 from django.utils.decorators import method_decorator
6 from django.views.decorators.csrf import csrf_protect
7 from django.views.decorators.csrf import csrf_protect
7 from django.views.generic.edit import FormMixin
8 from django.views.generic.edit import FormMixin
8
9
9 from boards.abstracts.settingsmanager import get_settings_manager, \
10 from boards.abstracts.settingsmanager import get_settings_manager, \
10 SETTING_SUBSCRIBE_BY_DEFAULT
11 SETTING_SUBSCRIBE_BY_DEFAULT
11 from boards.forms import PostForm, PlainErrorList
12 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post
13 from boards.models import Post
13 from boards.views.base import BaseBoardView, CONTEXT_FORM
14 from boards.views.base import BaseBoardView, CONTEXT_FORM
14 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
15 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
15
16
16 REQ_POST_ID = 'post_id'
17 REQ_POST_ID = 'post_id'
17
18
18 CONTEXT_LASTUPDATE = "last_update"
19 CONTEXT_LASTUPDATE = "last_update"
19 CONTEXT_THREAD = 'thread'
20 CONTEXT_THREAD = 'thread'
20 CONTEXT_MODE = 'mode'
21 CONTEXT_MODE = 'mode'
21 CONTEXT_OP = 'opening_post'
22 CONTEXT_OP = 'opening_post'
22 CONTEXT_FAVORITE = 'is_favorite'
23 CONTEXT_FAVORITE = 'is_favorite'
23 CONTEXT_RSS_URL = 'rss_url'
24 CONTEXT_RSS_URL = 'rss_url'
24
25
25
26
26 class ThreadView(BaseBoardView, FormMixin, DispatcherMixin):
27 class ThreadView(BaseBoardView, FormMixin, DispatcherMixin):
27
28
28 @method_decorator(csrf_protect)
29 @method_decorator(csrf_protect)
29 def get(self, request, post_id, form: PostForm=None):
30 def get(self, request, post_id, form: PostForm=None):
30 try:
31 try:
31 opening_post = Post.objects.get(id=post_id)
32 opening_post = Post.objects.get(id=post_id)
32 except ObjectDoesNotExist:
33 except ObjectDoesNotExist:
33 raise Http404
34 raise Http404
34
35
35 # If the tag is favorite, update the counter
36 # If the tag is favorite, update the counter
36 settings_manager = get_settings_manager(request)
37 settings_manager = get_settings_manager(request)
37 favorite = settings_manager.thread_is_fav(opening_post)
38 favorite = settings_manager.thread_is_fav(opening_post)
38 if favorite:
39 if favorite:
39 settings_manager.add_or_read_fav_thread(opening_post)
40 settings_manager.add_or_read_fav_thread(opening_post)
40
41
41 # If this is not OP, don't show it as it is
42 # If this is not OP, don't show it as it is
42 if not opening_post.is_opening():
43 if not opening_post.is_opening():
43 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
44 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
44 .get_absolute_url(), opening_post.id))
45 .get_absolute_url(), opening_post.id))
45
46
46 if not form:
47 if not form:
47 subscribe_by_default = settings_manager.get_setting(
48 subscribe_by_default = settings_manager.get_setting(
48 SETTING_SUBSCRIBE_BY_DEFAULT, False)
49 SETTING_SUBSCRIBE_BY_DEFAULT, False)
49 form = PostForm(error_class=PlainErrorList,
50 form = PostForm(error_class=PlainErrorList,
50 initial={'subscribe': subscribe_by_default})
51 initial={'subscribe': subscribe_by_default})
51
52
52 thread_to_show = opening_post.get_thread()
53 thread_to_show = opening_post.get_thread()
53
54
54 params = dict()
55 params = dict()
55
56
56 params[CONTEXT_FORM] = form
57 params[CONTEXT_FORM] = form
57 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
58 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
58 params[CONTEXT_THREAD] = thread_to_show
59 params[CONTEXT_THREAD] = thread_to_show
59 params[CONTEXT_MODE] = self.get_mode()
60 params[CONTEXT_MODE] = self.get_mode()
60 params[CONTEXT_OP] = opening_post
61 params[CONTEXT_OP] = opening_post
61 params[CONTEXT_FAVORITE] = favorite
62 params[CONTEXT_FAVORITE] = favorite
62 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
63 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
63
64
64 params.update(self.get_data(thread_to_show))
65 params.update(self.get_data(thread_to_show))
65
66
66 return render(request, self.get_template(), params)
67 return render(request, self.get_template(), params)
67
68
68 @method_decorator(csrf_protect)
69 @method_decorator(csrf_protect)
69 def post(self, request, post_id):
70 def post(self, request, post_id):
70 opening_post = get_object_or_404(Post, id=post_id)
71 opening_post = get_object_or_404(Post, id=post_id)
71
72
72 # If this is not OP, don't show it as it is
73 # If this is not OP, don't show it as it is
73 if not opening_post.is_opening():
74 if not opening_post.is_opening():
74 raise Http404
75 raise Http404
75
76
76 if PARAMETER_METHOD in request.POST:
77 if PARAMETER_METHOD in request.POST:
77 self.dispatch_method(request, opening_post)
78 result = self.dispatch_method(request, opening_post)
78
79
79 return redirect('thread', post_id) # FIXME Different for different modes
80 ajax = 'ajax' in request.GET
81 if ajax:
82 return HttpResponse(content=json.dumps(result))
83 else:
84 return redirect('thread', post_id) # FIXME Different for different modes
80
85
81 if not opening_post.get_thread().is_archived():
86 if not opening_post.get_thread().is_archived():
82 form = PostForm(request.POST, request.FILES,
87 form = PostForm(request.POST, request.FILES,
83 error_class=PlainErrorList,
88 error_class=PlainErrorList,
84 session=request.session)
89 session=request.session)
85
90
86 if form.is_valid():
91 if form.is_valid():
87 return Post.objects.create_from_form(request, form, opening_post)
92 return Post.objects.create_from_form(request, form, opening_post)
88 if form.need_to_ban:
93 if form.need_to_ban:
89 # Ban user because he is suspected to be a bot
94 # Ban user because he is suspected to be a bot
90 self._ban_current_user(request)
95 self._ban_current_user(request)
91
96
92 return self.get(request, post_id, form)
97 return self.get(request, post_id, form)
93
98
94 def get_data(self, thread) -> dict:
99 def get_data(self, thread) -> dict:
95 """
100 """
96 Returns context params for the view.
101 Returns context params for the view.
97 """
102 """
98
103
99 return dict()
104 return dict()
100
105
101 def get_template(self) -> str:
106 def get_template(self) -> str:
102 """
107 """
103 Gets template to show the thread mode on.
108 Gets template to show the thread mode on.
104 """
109 """
105
110
106 pass
111 pass
107
112
108 def get_mode(self) -> str:
113 def get_mode(self) -> str:
109 pass
114 pass
110
115
111 def subscribe(self, request, opening_post):
116 def subscribe(self, request, opening_post):
112 settings_manager = get_settings_manager(request)
117 settings_manager = get_settings_manager(request)
113 settings_manager.add_or_read_fav_thread(opening_post)
118 settings_manager.add_or_read_fav_thread(opening_post)
114
119
120 return {
121 'status': 'success',
122 'fav': 'true'
123 }
124
115 def unsubscribe(self, request, opening_post):
125 def unsubscribe(self, request, opening_post):
116 settings_manager = get_settings_manager(request)
126 settings_manager = get_settings_manager(request)
117 settings_manager.del_fav_thread(opening_post)
127 settings_manager.del_fav_thread(opening_post)
118
128
129 return {
130 'status': 'success',
131 'fav': 'false'
132 }
133
119 def get_rss_url(self, opening_id):
134 def get_rss_url(self, opening_id):
120 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
135 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
General Comments 0
You need to be logged in to leave comments. Login now