1 # chloro - personal space
2 # Copyright (C) 2019 Frederic Peters
4 # This program is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from django.conf import settings
21 from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
22 from django.contrib.syndication.views import Feed
23 from django.core.exceptions import PermissionDenied
24 from django.core.files.storage import default_storage
25 from django.http import Http404, HttpResponse, JsonResponse
26 from django.utils.feedgenerator import Atom1Feed
27 from django.utils.text import slugify
28 from django.views import View
29 from django.views.decorators.csrf import csrf_exempt
30 from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
31 from sorl.thumbnail.shortcuts import get_thumbnail
33 from .models import Note
36 class NoteView(DetailView):
39 def get(self, request, *args, **kwargs):
40 note = self.get_object()
41 if kwargs.get('year'):
42 # check date does match
43 creation = self.get_object().creation_timestamp
44 if (creation.year, creation.month, creation.day) != (
50 if not note.published and not request.user.is_staff:
51 raise PermissionDenied()
52 return super().get(request, *args, **kwargs)
55 class NoteEditView(UpdateView):
57 fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
60 class NoteApiSaveView(View):
61 http_method_names = ['post']
63 def post(self, request, *args, **kwargs):
64 note = Note.objects.get(slug=kwargs['slug'])
65 note.text = request.POST['text']
67 return HttpResponse('ok')
70 class NoteAddView(CreateView):
72 fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
75 class NoteDeleteView(DeleteView):
78 def get_success_url(self):
82 class HomeView(TemplateView):
83 template_name = 'phyll/home.html'
85 def get_context_data(self, **kwargs):
86 context = super().get_context_data(**kwargs)
87 context['posts'] = Note.objects.filter(published=True, included_in_feed=True).order_by(
90 context['recent_changes'] = Note.objects.filter(published=True, included_in_feed=True).order_by(
91 '-last_update_timestamp'
93 context['index'] = Note.objects.filter(slug='index').first()
97 class ArchivesView(ListView):
100 def get_queryset(self):
101 qs = super().get_queryset()
102 if not self.request.user.is_staff:
103 qs = qs.filter(published=True, included_in_feed=True)
107 class ListOnTagView(ListView):
110 def get_queryset(self):
111 qs = Note.objects.filter(tags__name__in=[self.kwargs['tag']])
112 if not self.request.user.is_staff:
113 qs = qs.filter(published=True)
117 class Atom1FeedWithBaseXml(Atom1Feed):
118 def root_attributes(self):
119 root_attributes = super().root_attributes()
120 scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(self.feed['feed_url'])
121 root_attributes['xml:base'] = urllib.parse.urlunparse((scheme, netloc, '/', params, query, fragment))
122 return root_attributes
125 class AtomFeed(Feed):
126 title = settings.SITE_TITLE
128 feed_type = Atom1FeedWithBaseXml
129 author_name = settings.SITE_AUTHOR
131 def get_object(self, request, *args, **kwargs):
132 self.sub = kwargs.get('sub', 'default')
133 return super().get_object(request, *args, **kwargs)
136 qs = Note.objects.filter(published=True, included_in_feed=True)
137 if self.sub == 'default':
139 elif self.sub == 'gnome-en':
140 qs = qs.filter(tags__name__in=['gnome']).filter(tags__name__in=['lang-en'])
142 qs = qs.filter(tags__name__in=[self.sub])
143 return qs.select_related()[:20]
145 def item_description(self, item):
148 def item_guid(self, item):
149 for tag in item.tags.all():
150 if tag.name.startswith('old-post-id-'):
151 return 'http://www.0d.be/posts/%s' % tag.name.split('-')[-1]
152 return 'https://www.0d.be' + item.get_absolute_url()
154 def item_title(self, item):
157 def item_pubdate(self, item):
158 return item.creation_timestamp
162 def ajax_upload(request, *args, **kwargs):
163 upload = request.FILES['upload']
164 upload_path = 'uploads'
165 if os.path.splitext(upload.name.lower())[-1] in ('.jpg', '.jpeg', '.png', '.gif', '.svg'):
166 upload_path = 'images'
167 saved_path = default_storage.save('%s/%s' % (upload_path, upload.name), upload)
168 url = '/media/' + saved_path
169 response = {'url': url, 'filename': upload.name}
170 if upload_path == 'images':
171 if default_storage.size(saved_path) > 500_000 and not upload.name.endswith('.svg'):
172 response['orig_url'] = url
174 response['url'] = get_thumbnail(saved_path, '1000', upscale=False).url
177 return JsonResponse(response)
181 def ajax_new_page(request, *args, **kwargs):
182 title = request.POST['title']
183 slug = slugify(title)
184 note, created = Note.objects.get_or_create(slug=slug)
187 note.text = '<p>...</p>'
191 'url': '/%s/' % note.slug,
192 'request_id': request.POST['request_id'],
197 def ajax_search(request, *args, **kwargs):
198 vector = SearchVector('title', weight='A', config='french') + SearchVector(
199 'plain_text', weight='B', config='french'
201 query = SearchQuery(request.GET.get('q', ''), config='french')
203 Note.objects.annotate(rank=SearchRank(vector, query)).filter(rank__gte=0.1).order_by('-rank')[:10]
206 {'data': [{'title': x.title, 'rank': x.rank, 'url': x.get_absolute_url()} for x in results]}