##// END OF EJS Templates
Search duplicates for file, not hash
neko259 -
r1827:bbdc98ff default
parent child Browse files
Show More
@@ -1,231 +1,231 b''
1 import re
1 import re
2
2
3 from PIL import Image
3 from PIL import Image
4
4
5 from django.contrib.staticfiles import finders
5 from django.contrib.staticfiles import finders
6 from django.contrib.staticfiles.templatetags.staticfiles import static
6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 from django.core.files.images import get_image_dimensions
7 from django.core.files.images import get_image_dimensions
8 from django.template.defaultfilters import filesizeformat
8 from django.template.defaultfilters import filesizeformat
9 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 from boards.utils import get_domain, cached_result
12 from boards.utils import get_domain, cached_result
13 from boards import settings
13 from boards import settings
14
14
15
15
16 FILE_STUB_IMAGE = 'images/file.png'
16 FILE_STUB_IMAGE = 'images/file.png'
17 FILE_STUB_URL = 'url'
17 FILE_STUB_URL = 'url'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19
19
20
20
21 FILE_TYPES_VIDEO = (
21 FILE_TYPES_VIDEO = (
22 'webm',
22 'webm',
23 'mp4',
23 'mp4',
24 'mpeg',
24 'mpeg',
25 'ogv',
25 'ogv',
26 )
26 )
27 FILE_TYPE_SVG = 'svg'
27 FILE_TYPE_SVG = 'svg'
28 FILE_TYPES_AUDIO = (
28 FILE_TYPES_AUDIO = (
29 'ogg',
29 'ogg',
30 'mp3',
30 'mp3',
31 'opus',
31 'opus',
32 )
32 )
33 FILE_TYPES_IMAGE = (
33 FILE_TYPES_IMAGE = (
34 'jpeg',
34 'jpeg',
35 'jpg',
35 'jpg',
36 'png',
36 'png',
37 'bmp',
37 'bmp',
38 'gif',
38 'gif',
39 )
39 )
40
40
41 PLAIN_FILE_FORMATS = {
41 PLAIN_FILE_FORMATS = {
42 'zip': 'archive',
42 'zip': 'archive',
43 'tar': 'archive',
43 'tar': 'archive',
44 'gz': 'archive',
44 'gz': 'archive',
45 'mid' : 'midi',
45 'mid' : 'midi',
46 }
46 }
47
47
48 URL_PROTOCOLS = {
48 URL_PROTOCOLS = {
49 'magnet': 'magnet',
49 'magnet': 'magnet',
50 }
50 }
51
51
52 CSS_CLASS_IMAGE = 'image'
52 CSS_CLASS_IMAGE = 'image'
53 CSS_CLASS_THUMB = 'thumb'
53 CSS_CLASS_THUMB = 'thumb'
54
54
55
55
56 def get_viewers():
56 def get_viewers():
57 return AbstractViewer.__subclasses__()
57 return AbstractViewer.__subclasses__()
58
58
59
59
60 def get_static_dimensions(filename):
60 def get_static_dimensions(filename):
61 file_path = finders.find(filename)
61 file_path = finders.find(filename)
62 return get_image_dimensions(file_path)
62 return get_image_dimensions(file_path)
63
63
64
64
65 # TODO Move this to utils
65 # TODO Move this to utils
66 def file_exists(filename):
66 def file_exists(filename):
67 return finders.find(filename) is not None
67 return finders.find(filename) is not None
68
68
69
69
70 class AbstractViewer:
70 class AbstractViewer:
71 def __init__(self, file, file_type, hash, url):
71 def __init__(self, file, file_type, hash, url):
72 self.file = file
72 self.file = file
73 self.file_type = file_type
73 self.file_type = file_type
74 self.hash = hash
74 self.hash = hash
75 self.url = url
75 self.url = url
76
76
77 @staticmethod
77 @staticmethod
78 def supports(file_type):
78 def supports(file_type):
79 return True
79 return True
80
80
81 def get_view(self):
81 def get_view(self):
82 search_host = settings.get('External', 'ImageSearchHost')
82 search_host = settings.get('External', 'ImageSearchHost')
83 if search_host:
83 if search_host:
84 if search_host.endswith('/'):
84 if search_host.endswith('/'):
85 search_host = search_host[:-1]
85 search_host = search_host[:-1]
86 search_url = search_host + self.file.url
86 search_url = search_host + self.file.url
87 else:
87 else:
88 search_url = ''
88 search_url = ''
89
89
90 return '<div class="image">'\
90 return '<div class="image">'\
91 '{}'\
91 '{}'\
92 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
92 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
93 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-hash="{}">πŸ” </a></div>'\
93 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
94 '</div>'.format(self.get_format_view(), self.file.url,
94 '</div>'.format(self.get_format_view(), self.file.url,
95 self.file_type, filesizeformat(self.file.size),
95 self.file_type, filesizeformat(self.file.size),
96 self.file_type, search_url, self.hash)
96 self.file_type, search_url, self.file.name)
97
97
98 def get_format_view(self):
98 def get_format_view(self):
99 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
99 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
100 file_name = FILE_FILEFORMAT.format(image_name)
100 file_name = FILE_FILEFORMAT.format(image_name)
101
101
102 if file_exists(file_name):
102 if file_exists(file_name):
103 image = file_name
103 image = file_name
104 else:
104 else:
105 image = FILE_STUB_IMAGE
105 image = FILE_STUB_IMAGE
106
106
107 w, h = get_static_dimensions(image)
107 w, h = get_static_dimensions(image)
108
108
109 return '<a href="{}">'\
109 return '<a href="{}">'\
110 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
110 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
111 '</a>'.format(self.file.url, static(image), w, h)
111 '</a>'.format(self.file.url, static(image), w, h)
112
112
113
113
114 class VideoViewer(AbstractViewer):
114 class VideoViewer(AbstractViewer):
115 @staticmethod
115 @staticmethod
116 def supports(file_type):
116 def supports(file_type):
117 return file_type in FILE_TYPES_VIDEO
117 return file_type in FILE_TYPES_VIDEO
118
118
119 def get_format_view(self):
119 def get_format_view(self):
120 return '<video width="200" height="150" controls src="{}"></video>'\
120 return '<video width="200" height="150" controls src="{}"></video>'\
121 .format(self.file.url)
121 .format(self.file.url)
122
122
123
123
124 class AudioViewer(AbstractViewer):
124 class AudioViewer(AbstractViewer):
125 @staticmethod
125 @staticmethod
126 def supports(file_type):
126 def supports(file_type):
127 return file_type in FILE_TYPES_AUDIO
127 return file_type in FILE_TYPES_AUDIO
128
128
129 def get_format_view(self):
129 def get_format_view(self):
130 return '<audio controls src="{}"></audio>'.format(self.file.url)
130 return '<audio controls src="{}"></audio>'.format(self.file.url)
131
131
132
132
133 class SvgViewer(AbstractViewer):
133 class SvgViewer(AbstractViewer):
134 @staticmethod
134 @staticmethod
135 def supports(file_type):
135 def supports(file_type):
136 return file_type == FILE_TYPE_SVG
136 return file_type == FILE_TYPE_SVG
137
137
138 def get_format_view(self):
138 def get_format_view(self):
139 return '<a class="thumb" href="{}">'\
139 return '<a class="thumb" href="{}">'\
140 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
140 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
141 '</a>'.format(self.file.url, self.file.url)
141 '</a>'.format(self.file.url, self.file.url)
142
142
143
143
144 class ImageViewer(AbstractViewer):
144 class ImageViewer(AbstractViewer):
145 @staticmethod
145 @staticmethod
146 def supports(file_type):
146 def supports(file_type):
147 return file_type in FILE_TYPES_IMAGE
147 return file_type in FILE_TYPES_IMAGE
148
148
149 def get_format_view(self):
149 def get_format_view(self):
150 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
150 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
151 filesizeformat(self.file.size))
151 filesizeformat(self.file.size))
152
152
153 try:
153 try:
154 width, height = get_image_dimensions(self.file.path)
154 width, height = get_image_dimensions(self.file.path)
155 except Exception:
155 except Exception:
156 # If the image is a decompression bomb, treat it as just a regular
156 # If the image is a decompression bomb, treat it as just a regular
157 # file
157 # file
158 return super().get_format_view()
158 return super().get_format_view()
159
159
160 preview_path = self.file.path.replace('.', '.200x150.')
160 preview_path = self.file.path.replace('.', '.200x150.')
161 pre_width, pre_height = get_image_dimensions(preview_path)
161 pre_width, pre_height = get_image_dimensions(preview_path)
162
162
163 split = self.file.url.rsplit('.', 1)
163 split = self.file.url.rsplit('.', 1)
164 w, h = 200, 150
164 w, h = 200, 150
165 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
165 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
166
166
167 return '<a class="{}" href="{full}">' \
167 return '<a class="{}" href="{full}">' \
168 '<img class="post-image-preview"' \
168 '<img class="post-image-preview"' \
169 ' src="{}"' \
169 ' src="{}"' \
170 ' alt="{}"' \
170 ' alt="{}"' \
171 ' width="{}"' \
171 ' width="{}"' \
172 ' height="{}"' \
172 ' height="{}"' \
173 ' data-width="{}"' \
173 ' data-width="{}"' \
174 ' data-height="{}" />' \
174 ' data-height="{}" />' \
175 '</a>' \
175 '</a>' \
176 .format(CSS_CLASS_THUMB,
176 .format(CSS_CLASS_THUMB,
177 thumb_url,
177 thumb_url,
178 self.hash,
178 self.hash,
179 str(pre_width),
179 str(pre_width),
180 str(pre_height), str(width), str(height),
180 str(pre_height), str(width), str(height),
181 full=self.file.url, image_meta=metadata)
181 full=self.file.url, image_meta=metadata)
182
182
183
183
184 class UrlViewer(AbstractViewer):
184 class UrlViewer(AbstractViewer):
185 @staticmethod
185 @staticmethod
186 def supports(file_type):
186 def supports(file_type):
187 return file_type is None
187 return file_type is None
188
188
189 def get_view(self):
189 def get_view(self):
190 return '<div class="image">' \
190 return '<div class="image">' \
191 '{}' \
191 '{}' \
192 '<div class="image-metadata">{}</div>' \
192 '<div class="image-metadata">{}</div>' \
193 '</div>'.format(self.get_format_view(), get_domain(self.url))
193 '</div>'.format(self.get_format_view(), get_domain(self.url))
194
194
195 def get_format_view(self):
195 def get_format_view(self):
196 protocol = self.url.split(':')[0]
196 protocol = self.url.split(':')[0]
197
197
198 domain = get_domain(self.url)
198 domain = get_domain(self.url)
199
199
200 if protocol in URL_PROTOCOLS:
200 if protocol in URL_PROTOCOLS:
201 url_image_name = URL_PROTOCOLS.get(protocol)
201 url_image_name = URL_PROTOCOLS.get(protocol)
202 elif domain:
202 elif domain:
203 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
203 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
204 else:
204 else:
205 url_image_name = FILE_STUB_URL
205 url_image_name = FILE_STUB_URL
206
206
207 image_path = 'images/{}.png'.format(url_image_name)
207 image_path = 'images/{}.png'.format(url_image_name)
208 image = static(image_path)
208 image = static(image_path)
209 w, h = get_static_dimensions(image_path)
209 w, h = get_static_dimensions(image_path)
210
210
211 return '<a href="{}">' \
211 return '<a href="{}">' \
212 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
212 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
213 '</a>'.format(self.url, image, w, h)
213 '</a>'.format(self.url, image, w, h)
214
214
215 @cached_result()
215 @cached_result()
216 def _find_image_for_domains(self, domain):
216 def _find_image_for_domains(self, domain):
217 """
217 """
218 Searches for the domain image for every domain level except top.
218 Searches for the domain image for every domain level except top.
219 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
219 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
220 example.co.uk, then co.uk
220 example.co.uk, then co.uk
221 """
221 """
222 levels = domain.split('.')
222 levels = domain.split('.')
223 while len(levels) > 1:
223 while len(levels) > 1:
224 domain = '.'.join(levels)
224 domain = '.'.join(levels)
225
225
226 filename = 'images/domains/{}.png'.format(domain)
226 filename = 'images/domains/{}.png'.format(domain)
227 if file_exists(filename):
227 if file_exists(filename):
228 return 'domains/' + domain
228 return 'domains/' + domain
229 else:
229 else:
230 del levels[0]
230 del levels[0]
231
231
@@ -1,220 +1,220 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 ITEM_VOLUME_LEVEL = 'volumeLevel';
26 var ITEM_VOLUME_LEVEL = 'volumeLevel';
27 var IMAGE_TYPES = ['png', 'jpg', 'jpeg', 'bmp'];
27 var IMAGE_TYPES = ['png', 'jpg', 'jpeg', 'bmp'];
28
28
29 /**
29 /**
30 * An email is a hidden file to prevent spam bots from posting. It has to be
30 * An email is a hidden file to prevent spam bots from posting. It has to be
31 * hidden.
31 * hidden.
32 */
32 */
33 function hideEmailFromForm() {
33 function hideEmailFromForm() {
34 $('.form-email').parent().parent().hide();
34 $('.form-email').parent().parent().hide();
35 }
35 }
36
36
37 /**
37 /**
38 * Highlight code blocks with code highlighter
38 * Highlight code blocks with code highlighter
39 */
39 */
40 function highlightCode(node) {
40 function highlightCode(node) {
41 node.find('pre code').each(function(i, e) {
41 node.find('pre code').each(function(i, e) {
42 hljs.highlightBlock(e);
42 hljs.highlightBlock(e);
43 });
43 });
44 }
44 }
45
45
46 function updateFavPosts(data) {
46 function updateFavPosts(data) {
47 var includePostBody = $('#fav-panel').is(":visible");
47 var includePostBody = $('#fav-panel').is(":visible");
48
48
49 var allNewPostCount = 0;
49 var allNewPostCount = 0;
50
50
51 if (includePostBody) {
51 if (includePostBody) {
52 var favoriteThreadPanel = $('#fav-panel');
52 var favoriteThreadPanel = $('#fav-panel');
53 favoriteThreadPanel.empty();
53 favoriteThreadPanel.empty();
54 }
54 }
55
55
56 $.each($.parseJSON(data), function (_, dict) {
56 $.each($.parseJSON(data), function (_, dict) {
57 var newPostCount = dict.new_post_count;
57 var newPostCount = dict.new_post_count;
58 allNewPostCount += newPostCount;
58 allNewPostCount += newPostCount;
59
59
60 if (includePostBody) {
60 if (includePostBody) {
61 var favThreadNode = $('<div class="post"></div>');
61 var favThreadNode = $('<div class="post"></div>');
62 favThreadNode.append($(dict.post_url));
62 favThreadNode.append($(dict.post_url));
63 favThreadNode.append(' ');
63 favThreadNode.append(' ');
64 favThreadNode.append($('<span class="title">' + dict.title + '</span>'));
64 favThreadNode.append($('<span class="title">' + dict.title + '</span>'));
65
65
66 if (newPostCount > 0) {
66 if (newPostCount > 0) {
67 favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)");
67 favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)");
68 }
68 }
69
69
70 favoriteThreadPanel.append(favThreadNode);
70 favoriteThreadPanel.append(favThreadNode);
71
71
72 addRefLinkPreview(favThreadNode[0]);
72 addRefLinkPreview(favThreadNode[0]);
73 }
73 }
74 });
74 });
75
75
76 var newPostCountNode = $('#new-fav-post-count');
76 var newPostCountNode = $('#new-fav-post-count');
77 if (allNewPostCount > 0) {
77 if (allNewPostCount > 0) {
78 newPostCountNode.text('(+' + allNewPostCount + ')');
78 newPostCountNode.text('(+' + allNewPostCount + ')');
79 newPostCountNode.show();
79 newPostCountNode.show();
80 } else {
80 } else {
81 newPostCountNode.hide();
81 newPostCountNode.hide();
82 }
82 }
83 }
83 }
84
84
85 function initFavPanel() {
85 function initFavPanel() {
86 var favPanelButton = $('#fav-panel-btn');
86 var favPanelButton = $('#fav-panel-btn');
87 if (favPanelButton.length > 0 && typeof SharedWorker != 'undefined') {
87 if (favPanelButton.length > 0 && typeof SharedWorker != 'undefined') {
88 var worker = new SharedWorker($('body').attr('data-update-script'));
88 var worker = new SharedWorker($('body').attr('data-update-script'));
89 worker.port.onmessage = function(e) {
89 worker.port.onmessage = function(e) {
90 updateFavPosts(e.data);
90 updateFavPosts(e.data);
91 };
91 };
92 worker.onerror = function(event){
92 worker.onerror = function(event){
93 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
93 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
94 };
94 };
95 worker.port.start();
95 worker.port.start();
96
96
97 $(favPanelButton).click(function() {
97 $(favPanelButton).click(function() {
98 var favPanel = $('#fav-panel');
98 var favPanel = $('#fav-panel');
99 favPanel.toggle();
99 favPanel.toggle();
100
100
101 worker.port.postMessage({ includePostBody: favPanel.is(':visible')});
101 worker.port.postMessage({ includePostBody: favPanel.is(':visible')});
102
102
103 return false;
103 return false;
104 });
104 });
105
105
106 $(document).on('keyup.removepic', function(e) {
106 $(document).on('keyup.removepic', function(e) {
107 if(e.which === 27) {
107 if(e.which === 27) {
108 $('#fav-panel').hide();
108 $('#fav-panel').hide();
109 }
109 }
110 });
110 });
111 }
111 }
112 }
112 }
113
113
114 function setVolumeLevel(level) {
114 function setVolumeLevel(level) {
115 localStorage.setItem(ITEM_VOLUME_LEVEL, level);
115 localStorage.setItem(ITEM_VOLUME_LEVEL, level);
116 }
116 }
117
117
118 function getVolumeLevel() {
118 function getVolumeLevel() {
119 var level = localStorage.getItem(ITEM_VOLUME_LEVEL);
119 var level = localStorage.getItem(ITEM_VOLUME_LEVEL);
120 if (level == null) {
120 if (level == null) {
121 level = 1.0;
121 level = 1.0;
122 }
122 }
123 return level
123 return level
124 }
124 }
125
125
126 function processVolumeUser(node) {
126 function processVolumeUser(node) {
127 if (!window.localStorage) return;
127 if (!window.localStorage) return;
128 node.prop("volume", getVolumeLevel());
128 node.prop("volume", getVolumeLevel());
129 node.on('volumechange', function(event) {
129 node.on('volumechange', function(event) {
130 setVolumeLevel(event.target.volume);
130 setVolumeLevel(event.target.volume);
131 $("video,audio").prop("volume", getVolumeLevel());
131 $("video,audio").prop("volume", getVolumeLevel());
132 });
132 });
133 }
133 }
134
134
135 /**
135 /**
136 * Add all scripts than need to work on post, when the post is added to the
136 * Add all scripts than need to work on post, when the post is added to the
137 * document.
137 * document.
138 */
138 */
139 function addScriptsToPost(post) {
139 function addScriptsToPost(post) {
140 addRefLinkPreview(post[0]);
140 addRefLinkPreview(post[0]);
141 highlightCode(post);
141 highlightCode(post);
142 processVolumeUser(post.find("video,audio"));
142 processVolumeUser(post.find("video,audio"));
143 }
143 }
144
144
145 /**
145 /**
146 * Fix compatibility issues with some rare browsers
146 * Fix compatibility issues with some rare browsers
147 */
147 */
148 function compatibilityCrutches() {
148 function compatibilityCrutches() {
149 if (window.operamini) {
149 if (window.operamini) {
150 $('#form textarea').each(function() { this.placeholder = ''; });
150 $('#form textarea').each(function() { this.placeholder = ''; });
151 }
151 }
152 }
152 }
153
153
154 function addContextMenu() {
154 function addContextMenu() {
155 $.contextMenu({
155 $.contextMenu({
156 selector: '.file-menu',
156 selector: '.file-menu',
157 trigger: 'left',
157 trigger: 'left',
158
158
159 build: function($trigger, e) {
159 build: function($trigger, e) {
160 var fileSearchUrl = $trigger.data('search-url');
160 var fileSearchUrl = $trigger.data('search-url');
161 var isImage = IMAGE_TYPES.indexOf($trigger.data('type')) > -1;
161 var isImage = IMAGE_TYPES.indexOf($trigger.data('type')) > -1;
162 var hasUrl = fileSearchUrl.length > 0;
162 var hasUrl = fileSearchUrl.length > 0;
163 return {
163 return {
164 items: {
164 items: {
165 duplicates: {
165 duplicates: {
166 name: gettext('Duplicates search'),
166 name: gettext('Duplicates search'),
167 callback: function(key, opts) {
167 callback: function(key, opts) {
168 window.location = '/feed/?image_hash=' + $trigger.data('hash');
168 window.location = '/feed/?image=' + $trigger.data('filename');
169 }
169 }
170 },
170 },
171 google: {
171 google: {
172 name: 'Google',
172 name: 'Google',
173 visible: isImage && hasUrl,
173 visible: isImage && hasUrl,
174 callback: function(key, opts) {
174 callback: function(key, opts) {
175 window.location = 'https://www.google.com/searchbyimage?image_url=' + fileSearchUrl;
175 window.location = 'https://www.google.com/searchbyimage?image_url=' + fileSearchUrl;
176 }
176 }
177 },
177 },
178 iqdb: {
178 iqdb: {
179 name: 'IQDB',
179 name: 'IQDB',
180 visible: isImage && hasUrl,
180 visible: isImage && hasUrl,
181 callback: function(key, opts) {
181 callback: function(key, opts) {
182 window.location = 'http://iqdb.org/?url=' + fileSearchUrl;
182 window.location = 'http://iqdb.org/?url=' + fileSearchUrl;
183 }
183 }
184 },
184 },
185 tineye: {
185 tineye: {
186 name: 'TinEye',
186 name: 'TinEye',
187 visible: isImage && hasUrl,
187 visible: isImage && hasUrl,
188 callback: function(key, opts) {
188 callback: function(key, opts) {
189 window.location = 'http://tineye.com/search?url=' + fileSearchUrl;
189 window.location = 'http://tineye.com/search?url=' + fileSearchUrl;
190 }
190 }
191 }
191 }
192 },
192 },
193 };
193 };
194 }
194 }
195 });
195 });
196 }
196 }
197
197
198 $( document ).ready(function() {
198 $( document ).ready(function() {
199 hideEmailFromForm();
199 hideEmailFromForm();
200
200
201 $("a[href='#top']").click(function() {
201 $("a[href='#top']").click(function() {
202 $("html, body").animate({ scrollTop: 0 }, "slow");
202 $("html, body").animate({ scrollTop: 0 }, "slow");
203 return false;
203 return false;
204 });
204 });
205
205
206 addImgPreview();
206 addImgPreview();
207
207
208 addRefLinkPreview();
208 addRefLinkPreview();
209
209
210 highlightCode($(document));
210 highlightCode($(document));
211
211
212 initFavPanel();
212 initFavPanel();
213
213
214 var volumeUsers = $("video,audio");
214 var volumeUsers = $("video,audio");
215 processVolumeUser(volumeUsers);
215 processVolumeUser(volumeUsers);
216
216
217 addContextMenu();
217 addContextMenu();
218
218
219 compatibilityCrutches();
219 compatibilityCrutches();
220 });
220 });
@@ -1,151 +1,151 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.shortcuts import render
2 from django.shortcuts import render
3
3
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.abstracts.settingsmanager import get_settings_manager
6 from boards.abstracts.settingsmanager import get_settings_manager
7 from boards.models import Post
7 from boards.models import Post
8 from boards.views.base import BaseBoardView
8 from boards.views.base import BaseBoardView
9 from boards.views.posting_mixin import PostMixin
9 from boards.views.posting_mixin import PostMixin
10
10
11 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
11 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
12
12
13 PARAMETER_CURRENT_PAGE = 'current_page'
13 PARAMETER_CURRENT_PAGE = 'current_page'
14 PARAMETER_PAGINATOR = 'paginator'
14 PARAMETER_PAGINATOR = 'paginator'
15 PARAMETER_POSTS = 'posts'
15 PARAMETER_POSTS = 'posts'
16 PARAMETER_QUERIES = 'queries'
16 PARAMETER_QUERIES = 'queries'
17
17
18 PARAMETER_PREV_LINK = 'prev_page_link'
18 PARAMETER_PREV_LINK = 'prev_page_link'
19 PARAMETER_NEXT_LINK = 'next_page_link'
19 PARAMETER_NEXT_LINK = 'next_page_link'
20
20
21 TEMPLATE = 'boards/feed.html'
21 TEMPLATE = 'boards/feed.html'
22 DEFAULT_PAGE = 1
22 DEFAULT_PAGE = 1
23
23
24
24
25 class FeedFilter:
25 class FeedFilter:
26 @staticmethod
26 @staticmethod
27 def get_filtered_posts(request, posts):
27 def get_filtered_posts(request, posts):
28 return posts
28 return posts
29
29
30 @staticmethod
30 @staticmethod
31 def get_query(request):
31 def get_query(request):
32 return None
32 return None
33
33
34
34
35 class TripcodeFilter(FeedFilter):
35 class TripcodeFilter(FeedFilter):
36 @staticmethod
36 @staticmethod
37 def get_filtered_posts(request, posts):
37 def get_filtered_posts(request, posts):
38 filtered_posts = posts
38 filtered_posts = posts
39 tripcode = request.GET.get('tripcode', None)
39 tripcode = request.GET.get('tripcode', None)
40 if tripcode:
40 if tripcode:
41 filtered_posts = filtered_posts.filter(tripcode=tripcode)
41 filtered_posts = filtered_posts.filter(tripcode=tripcode)
42 return filtered_posts
42 return filtered_posts
43
43
44 @staticmethod
44 @staticmethod
45 def get_query(request):
45 def get_query(request):
46 tripcode = request.GET.get('tripcode', None)
46 tripcode = request.GET.get('tripcode', None)
47 if tripcode:
47 if tripcode:
48 return 'Tripcode: {}'.format(tripcode)
48 return 'Tripcode: {}'.format(tripcode)
49
49
50
50
51 class FavoritesFilter(FeedFilter):
51 class FavoritesFilter(FeedFilter):
52 @staticmethod
52 @staticmethod
53 def get_filtered_posts(request, posts):
53 def get_filtered_posts(request, posts):
54 filtered_posts = posts
54 filtered_posts = posts
55
55
56 favorites = 'favorites' in request.GET
56 favorites = 'favorites' in request.GET
57 if favorites:
57 if favorites:
58 settings_manager = get_settings_manager(request)
58 settings_manager = get_settings_manager(request)
59 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
59 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
60 fav_threads = [op.get_thread() for op in fav_thread_ops]
60 fav_threads = [op.get_thread() for op in fav_thread_ops]
61 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
61 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
62 return filtered_posts
62 return filtered_posts
63
63
64
64
65 class IpFilter(FeedFilter):
65 class IpFilter(FeedFilter):
66 @staticmethod
66 @staticmethod
67 def get_filtered_posts(request, posts):
67 def get_filtered_posts(request, posts):
68 filtered_posts = posts
68 filtered_posts = posts
69
69
70 ip = request.GET.get('ip', None)
70 ip = request.GET.get('ip', None)
71 if ip and request.user.has_perm('post_delete'):
71 if ip and request.user.has_perm('post_delete'):
72 filtered_posts = filtered_posts.filter(poster_ip=ip)
72 filtered_posts = filtered_posts.filter(poster_ip=ip)
73 return filtered_posts
73 return filtered_posts
74
74
75 @staticmethod
75 @staticmethod
76 def get_query(request):
76 def get_query(request):
77 ip = request.GET.get('ip', None)
77 ip = request.GET.get('ip', None)
78 if ip:
78 if ip:
79 return 'IP: {}'.format(ip)
79 return 'IP: {}'.format(ip)
80
80
81
81
82 class HashFilter(FeedFilter):
82 class ImageFilter(FeedFilter):
83 @staticmethod
83 @staticmethod
84 def get_filtered_posts(request, posts):
84 def get_filtered_posts(request, posts):
85 filtered_posts = posts
85 filtered_posts = posts
86
86
87 image_hash = request.GET.get('image_hash', None)
87 image = request.GET.get('image', None)
88 if image_hash:
88 if image:
89 filtered_posts = filtered_posts.filter(attachments__hash=image_hash)
89 filtered_posts = filtered_posts.filter(attachments__file=image)
90 return filtered_posts
90 return filtered_posts
91
91
92 @staticmethod
92 @staticmethod
93 def get_query(request):
93 def get_query(request):
94 image_hash = request.GET.get('image_hash', None)
94 image = request.GET.get('image', None)
95 if image_hash:
95 if image:
96 return 'Hash: {}'.format(image_hash)
96 return 'File: {}'.format(image)
97
97
98
98
99 class FeedView(PostMixin, BaseBoardView):
99 class FeedView(PostMixin, BaseBoardView):
100 filters = (
100 filters = (
101 TripcodeFilter,
101 TripcodeFilter,
102 FavoritesFilter,
102 FavoritesFilter,
103 IpFilter,
103 IpFilter,
104 HashFilter,
104 ImageFilter,
105 )
105 )
106
106
107 def get(self, request):
107 def get(self, request):
108 page = request.GET.get('page', DEFAULT_PAGE)
108 page = request.GET.get('page', DEFAULT_PAGE)
109
109
110 params = self.get_context_data(request=request)
110 params = self.get_context_data(request=request)
111
111
112 settings_manager = get_settings_manager(request)
112 settings_manager = get_settings_manager(request)
113
113
114 posts = Post.objects.exclude(
114 posts = Post.objects.exclude(
115 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
115 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
116 '-pub_time').prefetch_related('attachments', 'thread')
116 '-pub_time').prefetch_related('attachments', 'thread')
117 queries = []
117 queries = []
118 for filter in self.filters:
118 for filter in self.filters:
119 posts = filter.get_filtered_posts(request, posts)
119 posts = filter.get_filtered_posts(request, posts)
120 query = filter.get_query(request)
120 query = filter.get_query(request)
121 if query:
121 if query:
122 queries.append(query)
122 queries.append(query)
123 params[PARAMETER_QUERIES] = queries
123 params[PARAMETER_QUERIES] = queries
124
124
125 paginator = get_paginator(posts, POSTS_PER_PAGE)
125 paginator = get_paginator(posts, POSTS_PER_PAGE)
126 paginator.current_page = int(page)
126 paginator.current_page = int(page)
127
127
128 params[PARAMETER_POSTS] = paginator.page(page).object_list
128 params[PARAMETER_POSTS] = paginator.page(page).object_list
129
129
130 paginator.set_url(reverse('feed'), request.GET.dict())
130 paginator.set_url(reverse('feed'), request.GET.dict())
131
131
132 self.get_page_context(paginator, params, page)
132 self.get_page_context(paginator, params, page)
133
133
134 return render(request, TEMPLATE, params)
134 return render(request, TEMPLATE, params)
135
135
136 # TODO Dedup this into PagedMixin
136 # TODO Dedup this into PagedMixin
137 def get_page_context(self, paginator, params, page):
137 def get_page_context(self, paginator, params, page):
138 """
138 """
139 Get pagination context variables
139 Get pagination context variables
140 """
140 """
141
141
142 params[PARAMETER_PAGINATOR] = paginator
142 params[PARAMETER_PAGINATOR] = paginator
143 current_page = paginator.page(int(page))
143 current_page = paginator.page(int(page))
144 params[PARAMETER_CURRENT_PAGE] = current_page
144 params[PARAMETER_CURRENT_PAGE] = current_page
145 if current_page.has_previous():
145 if current_page.has_previous():
146 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
146 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
147 current_page.previous_page_number())
147 current_page.previous_page_number())
148 if current_page.has_next():
148 if current_page.has_next():
149 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
149 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
150 current_page.next_page_number())
150 current_page.next_page_number())
151
151
General Comments 0
You need to be logged in to leave comments. Login now