fixes
[redakcja.git] / src / documents / forms.py
1 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 from django.db.models import Count
5 from django import forms
6 from django.utils.translation import ugettext_lazy as _
7 from django.conf import settings
8 from slugify import slugify
9 from .constants import MASTERS
10 from .models import Book, Chunk, Image, User
11 from .docx import xml_from_docx
12
13 class DocumentCreateForm(forms.ModelForm):
14     """
15         Form used for creating new documents.
16     """
17     file = forms.FileField(required=False)
18     text = forms.CharField(required=False, widget=forms.Textarea)
19     docx = forms.FileField(required=False)
20
21     class Meta:
22         model = Book
23         exclude = ['parent', 'parent_number', 'project']
24
25     def __init__(self, *args, **kwargs):
26         super(DocumentCreateForm, self).__init__(*args, **kwargs)
27         self.fields['slug'].widget.attrs={'class': 'autoslug'}
28         self.fields['slug'].required = False
29         self.fields['gallery'].widget.attrs={'class': 'autoslug'}
30         self.fields['title'].widget.attrs={'class': 'autoslug-source'}
31         self.fields['title'].required = False
32
33     def clean(self):
34         super(DocumentCreateForm, self).clean()
35         file = self.cleaned_data['file']
36
37         if file is not None:
38             try:
39                 self.cleaned_data['text'] = file.read().decode('utf-8')
40             except UnicodeDecodeError:
41                 raise forms.ValidationError(_("Text file must be UTF-8 encoded."))
42
43         docx = self.cleaned_data['docx']
44         if docx is not None:
45             try:
46                 text, meta = xml_from_docx(docx)
47             except Exception as e:
48                 raise forms.ValidationError(e)
49             else:
50                 self.cleaned_data['text'] = text
51                 if not self.cleaned_data['title']:
52                     self.cleaned_data['title'] = meta.get('title', '')
53                 if not self.cleaned_data['slug']:
54                     self.cleaned_data['slug'] = slugify(meta.get('title', ''))
55
56         if not self.cleaned_data["title"]:
57             self._errors["title"] = self.error_class([_("Title not set")])
58
59         if not self.cleaned_data["slug"]:
60             self._errors["slug"] = self.error_class([_("Slug not set")])
61
62         if not self.cleaned_data["text"]:
63             self._errors["text"] = self.error_class([_("You must either enter text or upload a file")])
64
65         return self.cleaned_data
66
67
68 class DocumentsUploadForm(forms.Form):
69     """
70         Form used for uploading new documents.
71     """
72     file = forms.FileField(required=True, label=_('ZIP file'))
73     dirs = forms.BooleanField(label=_('Directories are documents in chunks'),
74             widget = forms.CheckboxInput(attrs={'disabled':'disabled'}))
75
76     def clean(self):
77         file = self.cleaned_data['file']
78
79         import zipfile
80         try:
81             z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
82         except zipfile.BadZipfile:
83             raise forms.ValidationError("Should be a ZIP file.")
84         if z.testzip():
85             raise forms.ValidationError("ZIP file corrupt.")
86
87         return self.cleaned_data
88
89
90 class ChunkForm(forms.ModelForm):
91     """
92         Form used for editing a chunk.
93     """
94     user = forms.ModelChoiceField(queryset=
95         User.objects.annotate(count=Count('chunk')).
96         order_by('last_name', 'first_name'), required=False,
97         label=_('Assigned to')) 
98
99     class Meta:
100         model = Chunk
101         fields = ['title', 'slug', 'gallery_start', 'user', 'stage']
102         exclude = ['number']
103
104     def __init__(self, *args, **kwargs):
105         super(ChunkForm, self).__init__(*args, **kwargs)
106         self.fields['gallery_start'].widget.attrs={'class': 'number-input'}
107         self.fields['slug'].widget.attrs={'class': 'autoslug'}
108         self.fields['title'].widget.attrs={'class': 'autoslug-source'}
109
110     def clean_slug(self):
111         slug = self.cleaned_data['slug']
112         try:
113             chunk = Chunk.objects.get(book=self.instance.book, slug=slug)
114         except Chunk.DoesNotExist:
115             return slug
116         if chunk == self.instance:
117             return slug
118         raise forms.ValidationError(_('Chunk with this slug already exists'))
119
120
121 class ChunkAddForm(ChunkForm):
122     """
123         Form used for adding a chunk to a document.
124     """
125
126     def clean_slug(self):
127         slug = self.cleaned_data['slug']
128         try:
129             user = Chunk.objects.get(book=self.instance.book, slug=slug)
130         except Chunk.DoesNotExist:
131             return slug
132         raise forms.ValidationError(_('Chunk with this slug already exists'))
133
134
135 class BookAppendForm(forms.Form):
136     """
137         Form for appending a book to another book.
138         It means moving all chunks from book A to book B and deleting A.
139     """
140     append_to = forms.ModelChoiceField(queryset=Book.objects.all(),
141             label=_("Append to"))
142
143     def __init__(self, book, *args, **kwargs):
144         ret =  super(BookAppendForm, self).__init__(*args, **kwargs)
145         self.fields['append_to'].queryset = Book.objects.exclude(pk=book.pk)
146         return ret
147
148
149 class BookForm(forms.ModelForm):
150     """Form used for editing a Book."""
151
152     class Meta:
153         model = Book
154         exclude = ['project']
155
156     def __init__(self, *args, **kwargs):
157         ret = super(BookForm, self).__init__(*args, **kwargs)
158         self.fields['slug'].widget.attrs.update({"class": "autoslug"})
159         self.fields['title'].widget.attrs.update({"class": "autoslug-source"})
160         return ret
161
162     def save(self, **kwargs):
163         orig_instance = Book.objects.get(pk=self.instance.pk)
164         old_gallery = orig_instance.gallery
165         new_gallery = self.cleaned_data['gallery']
166         if new_gallery and old_gallery and new_gallery != old_gallery:
167             import shutil
168             import os.path
169             from django.conf import settings
170             shutil.move(orig_instance.gallery_path(),
171                         os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, new_gallery))
172         super(BookForm, self).save(**kwargs)
173
174
175 class ReadonlyBookForm(BookForm):
176     """Form used for not editing a Book."""
177
178     def __init__(self, *args, **kwargs):
179         ret = super(ReadonlyBookForm, self).__init__(*args, **kwargs)
180         for field in self.fields.values():
181             field.widget.attrs.update({"disabled": "disabled"})
182         return ret
183
184
185 class ChooseMasterForm(forms.Form):
186     """
187         Form used for fixing the chunks in a book.
188     """
189
190     master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
191
192
193 class ImageForm(forms.ModelForm):
194     """Form used for editing an Image."""
195     user = forms.ModelChoiceField(queryset=
196         User.objects.annotate(count=Count('chunk')).
197         order_by('-count', 'last_name', 'first_name'), required=False,
198         label=_('Assigned to')) 
199
200     class Meta:
201         model = Image
202         fields = ['title', 'slug', 'user', 'stage']
203
204     def __init__(self, *args, **kwargs):
205         super(ImageForm, self).__init__(*args, **kwargs)
206         self.fields['slug'].widget.attrs={'class': 'autoslug'}
207         self.fields['title'].widget.attrs={'class': 'autoslug-source'}
208
209
210 class ReadonlyImageForm(ImageForm):
211     """Form used for not editing an Image."""
212
213     def __init__(self, *args, **kwargs):
214         super(ReadonlyImageForm, self).__init__(*args, **kwargs)
215         for field in self.fields.values():
216             field.widget.attrs.update({"disabled": "disabled"})
217
218
219 class MarkFinalForm(forms.Form):
220     username = forms.CharField(initial=settings.LITERARY_DIRECTOR_USERNAME)
221     comment = forms.CharField(initial=u'Ostateczna akceptacja merytoryczna przez kierownika literackiego.')
222     books = forms.CharField(widget=forms.Textarea, help_text=u'linki do książek w redakcji, po jednym na wiersz')
223
224     def clean_books(self):
225         books_value = self.cleaned_data['books']
226         slugs = [line.strip().strip('/').split('/')[-1] for line in books_value.split('\n') if line.strip()]
227         books = Book.objects.filter(slug__in=slugs)
228         if len(books) != len(slugs):
229             raise forms.ValidationError(
230                 'Incorrect slug(s): %s' % ' '.join(slug for slug in slugs if not Book.objects.filter(slug=slug)))
231         return books
232
233     def clean_username(self):
234         username = self.cleaned_data['username']
235         if not User.objects.filter(username=username):
236             raise forms.ValidationError('Invalid username')
237         return username
238
239     def save(self):
240         for book in self.cleaned_data['books']:
241             for chunk in book.chunk_set.all():
242                 src = chunk.head.materialize()
243                 chunk.commit(
244                     text=src,
245                     author=User.objects.get(username=self.cleaned_data['username']),
246                     description=self.cleaned_data['comment'],
247                     tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
248                     publishable=True
249                 )
250
251
252 class PublishOptionsForm(forms.Form):
253     days = forms.IntegerField(label=u'po ilu dniach udostępnienić (0 = od razu)', min_value=0, initial=0)
254     hidden = forms.BooleanField(label='tekst ukryty przed wyszukiwaniem', required=False, initial=False)
255     beta = forms.BooleanField(label=u'Opublikuj na wersji testowej', required=False)