limit number of db queries
[qcg-portal.git] / qcg / views.py
1 # coding=utf-8
2 from datetime import datetime, timedelta
3
4 from django.conf import settings
5 from django.contrib import messages
6 from django.contrib.auth import REDIRECT_FIELD_NAME
7 from django.contrib.auth.decorators import login_required
8 from django.core.exceptions import SuspiciousOperation
9 from django.core.urlresolvers import reverse
10 from django.db.models import Q
11 from django.http import HttpResponse, QueryDict
12 from django.shortcuts import render, get_object_or_404, redirect
13 from django.utils.html import format_html
14 from django.utils.http import urlencode
15 from django.utils.timezone import UTC
16 from django.views.decorators.http import require_POST
17 from django_openid_auth.views import make_consumer
18 from openid.extensions import ax
19 from pyqcg.utils import PyqcgException
20
21 from filex.forms import HostPathNameForm, RenameForm, ArchiveForm, HostPathForm
22 from filex.ftp import FTPOperation, FTPError
23 from filex.views import make_url
24 from qcg.forms import FiltersForm, ColumnsForm, JobDescriptionForm, EnvFormSet, JobTemplateForm
25 from qcg.models import JobTemplate
26 <<<<<<< HEAD
27 from qcg.utils import paginator_context, to_job_desc, to_form_data, generate_md_inputfile
28 =======
29 from qcg.utils import paginator_context, to_job_desc, to_form_data, restricted
30 >>>>>>> 4a588e0... set Cache-Control header for restricted views
31 from qcg.service import update_user_data, update_job, cancel, clean
32
33
34 from django.utils import encoding
35 import string
36 import random
37 import os
38 from qcg import constants
39 from qcg.utils import chunks
40 # for Debugging
41 from pprint import pprint
42
43 def index(request):
44     return render(request, 'qcg/base.html')
45
46
47 def openid_begin(request):
48     openid_request = make_consumer(request).begin(settings.OPENID_SSO_SERVER_URL)
49
50     fetch_request = ax.FetchRequest()
51     for (attr, alias) in [('http://axschema.org/namePerson/friendly', 'nickname'),
52                           ('http://axschema.org/contact/email', 'email'),
53                           ('http://axschema.org/namePerson', 'fullname'),
54                           ('http://openid.plgrid.pl/certificate/proxy', 'proxy'),
55                           ('http://openid.plgrid.pl/certificate/userCert', 'userCert'),
56                           ('http://openid.plgrid.pl/certificate/proxyPrivKey', 'proxyPrivKey'),
57                           ('http://openid.plgrid.pl/POSTresponse', 'POSTresponse')]:
58         fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True))
59     openid_request.addExtension(fetch_request)
60
61     return_to = request.build_absolute_uri(reverse('openid-complete'))
62
63     redirect_to = request.GET.get(REDIRECT_FIELD_NAME)
64     if redirect_to:
65         return_to += '?' + urlencode({REDIRECT_FIELD_NAME: redirect_to})
66
67     return HttpResponse(openid_request.htmlMarkup(request.build_absolute_uri('/'), return_to))
68
69
70 search_fields = ('status_description', 'type', 'note', 'task_id', 'job__note', 'job__project', 'job__job_id',
71                  'allocations__status_description', 'allocations__processes_group_id', 'allocations__comment')
72
73
74 def parse_date(string):
75     return datetime.strptime(string.strip(), "%d.%m.%Y").replace(tzinfo=UTC())
76
77
78 @restricted
79 def jobs_list(request):
80     update_user_data(request.user, request.session['proxy'])
81
82     tasks = request.user.tasks.order_by('-job__submission_time', '-submission_time') \
83         .select_related('job').prefetch_related('allocations__nodes')
84
85     filters = FiltersForm(request.GET)
86     selected_filters = []
87     if filters.is_valid():
88         keywords = filters.cleaned_data['keywords']
89         status = filters.cleaned_data['status']
90         host = filters.cleaned_data['host']
91         purged = filters.cleaned_data['purged']
92         submission = filters.cleaned_data['submission']
93         finish = filters.cleaned_data['finish']
94
95         if keywords:
96             and_query = Q()
97
98             for q in keywords.split():
99                 or_query = Q()
100                 for field in search_fields:
101                     or_query |= Q(**{field + '__icontains': q})
102                 and_query &= or_query
103
104             tasks = tasks.filter(and_query)
105             selected_filters.append((keywords, 'keywords', keywords))
106
107         if status:
108             statuses = []
109             for s in status:
110                 si = int(s)
111                 statuses.extend(FiltersForm.STATUS_MAP[si])
112                 selected_filters.append((FiltersForm.STATUS_CHOICES_DICT[si], 'status', s))
113
114             tasks = tasks.filter(status__in=statuses)
115
116         if host:
117             tasks = tasks.filter(allocations__host_name__in=host)
118
119             host_dict = dict(filters.fields['host'].choices)
120             for h in host:
121                 selected_filters.append((host_dict[h], 'host', h))
122
123         if isinstance(purged, bool):
124             tasks = tasks.filter(purged=purged)
125
126             if purged:
127                 selected_filters.append((u"Usunięty katalog roboczy", 'purged', '1'))
128             else:
129                 selected_filters.append((u"Istniejący katalog roboczy", 'purged', '0'))
130
131         if submission:
132             start, end = submission.split('-')
133
134             tasks = tasks.filter(submission_time__gte=parse_date(start),
135                                  submission_time__lte=parse_date(end) + timedelta(days=1))
136             selected_filters.append((u'Data zlecenia: ' + submission, 'submission', submission))
137
138         if finish:
139             start, end = finish.split('-')
140
141             tasks = tasks.filter(finish_time__gte=parse_date(start),
142                                  finish_time__lte=parse_date(end) + timedelta(days=1))
143             selected_filters.append((u'Data zakończenia: ' + finish, 'finish', finish))
144
145         tasks = tasks.distinct()
146
147     checked_status = {i: widget.is_checked() for i, widget in enumerate(filters['status'])}
148
149     columns = ColumnsForm(QueryDict(request.COOKIES.get('columns', '')))
150
151     displayed = None
152     if columns.is_valid():
153         displayed = {int(c) for c in columns.cleaned_data['columns']}
154
155     # if invalid or empty
156     if not displayed:
157         displayed = set(columns.fields['columns'].initial)
158
159     context = {'filters': filters, 'checked_status': checked_status, 'selected_filters': selected_filters,
160                'columns': ColumnsForm(initial={'columns': displayed}), 'displayed': displayed}
161
162     context.update(paginator_context(request, tasks))
163
164     return render(request, 'qcg/jobs.html', context)
165
166
167 @restricted
168 def job_details(request, job_id):
169     job = get_object_or_404(request.user.jobs.select_related('owner').prefetch_related('tasks'), job_id=job_id)
170
171     return render(request, 'qcg/job.html', process_details(request, job))
172
173
174 @restricted
175 def task_details(request, job_id, task_id):
176     task = get_object_or_404(request.user.tasks.select_related('job__owner').prefetch_related('allocations'),
177                              job__job_id=job_id, task_id=task_id)
178
179     return render(request, 'qcg/task.html', process_details(request, task.job, task))
180
181
182 def process_details(request, job, task=None):
183     if not job.terminated or not job.purged:
184         update_job(job, request.session['proxy'])
185
186     form_data = to_form_data(job.qcg_description)
187     env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
188
189     form = JobDescriptionForm(initial=form_data)
190     env_formset = EnvFormSet(initial=env_formset_data)
191     template_form = JobTemplateForm(initial={'name': (task and task.note) or u'Szablon z {}'.format(job.job_id)})
192
193     for field in form:
194         field.field.widget.attrs['disabled'] = 'True'
195     for env_form in env_formset:
196         for field in env_form:
197             field.field.widget.attrs['disabled'] = 'True'
198
199     return {'job': job, 'task': task, 'form': form, 'env_formset': env_formset, 'template_form': template_form}
200
201
202 def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
203     return ''.join(random.choice(chars) for _ in range(size))
204
205 @restricted
206 def job_submit(request, template_id=None):
207     save_template = 'save-template' in request.POST
208     template = get_object_or_404(request.user.templates, id=template_id) if template_id is not None else None
209
210     if template is not None:
211         form_data = to_form_data(template.description)
212         env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
213     else:
214         form_data, env_formset_data = None, None
215
216     if request.method == 'POST':
217         form = JobDescriptionForm(request.POST, initial=form_data)
218         env_formset = EnvFormSet(request.POST, initial=env_formset_data)
219         template_form = JobTemplateForm(request.POST, prefix='template', instance=template)
220
221         if form.is_valid() and env_formset.is_valid() and (not save_template or template_form.is_valid()):
222             params = form.cleaned_data
223             params['env_variables'] = [(env['name'], env['value'])
224                                        for env in env_formset.cleaned_data if env and not env['DELETE']]
225
226             # Generowanie pliku inputowego
227             md_input=generate_md_inputfile(params)
228             url=os.path.splitext(params['pdb_file'])[0]+'_MD_genarated.inp'
229             params['master_file']=url
230             params['stage_in'].append(params['pdb_file'])
231             # Upload
232             ftp = FTPOperation(request.session['proxy'])
233             ftp.put(url)
234             for chunk in chunks('\n'.join([str(i) for i in md_input] ), 4096):
235                 ftp.stream.put(chunk)
236             ftp.stream.put(None)
237             ftp.wait()
238             
239             params['persistent'] = True
240
241             job_desc = to_job_desc(params, request.session['proxy'])
242
243
244             if save_template:
245                 template = template_form.save(commit=False)
246
247                 template.owner = request.user
248                 template.description = job_desc.xml_description
249                 template.save()
250
251                 return redirect(template)
252
253             # wyłączenie wysyłania zadania - zakomentować poniżej
254             
255             job = job_desc.submit()
256
257             messages.success(request,
258                              format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
259                                          u'Zlecono zadanie <em>{}</em>.', job.job_id))
260             
261             return redirect('jobs')
262     else:
263         form = JobDescriptionForm(initial=form_data)
264         env_formset = EnvFormSet(initial=env_formset_data)
265         template_form = JobTemplateForm(prefix='template', instance=template)
266
267     errors = form.errors or (env_formset.is_bound and not env_formset.is_valid) or (
268         save_template and template_form.errors)
269
270     return render(request, 'qcg/job_submit.html', {'form': form, 'env_formset': env_formset, 'errors': errors,
271                                                    'template_form': template_form, 'template': template})
272
273
274 @require_POST
275 @login_required
276 def job_cancel(request, job_id):
277     return obj_cancel(request, get_object_or_404(request.user.jobs, job_id=job_id))
278
279
280 @require_POST
281 @login_required
282 def task_cancel(request, job_id, task_id):
283     return obj_cancel(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
284
285
286 def obj_cancel(request, obj):
287     try:
288         cancel(obj, request.session['proxy'])
289     except PyqcgException as e:
290         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
291                                             e.message))
292     else:
293         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
294                                               'Zadanie anulowano.'))
295
296     return redirect(obj)
297
298
299 @require_POST
300 @login_required
301 def job_clean(request, job_id):
302     return obj_clean(request, get_object_or_404(request.user.jobs, job_id=job_id))
303
304
305 @require_POST
306 @login_required
307 def task_clean(request, job_id, task_id):
308     return obj_clean(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
309
310
311 def obj_clean(request, obj):
312     try:
313         clean(obj, request.session['proxy'])
314     except PyqcgException as e:
315         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
316                                             e.message))
317     else:
318         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
319                                               'Usunięto katalog roboczy.'))
320
321     return redirect(obj)
322
323
324 @restricted
325 def gridftp(request):
326     return render(request, 'qcg/gridftp.html',
327                   {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(), 'archive_form': ArchiveForm()})
328
329
330 @restricted
331 def gridftp_upload(request):
332     form = HostPathForm(request.GET)
333
334     if not form.is_valid():
335         raise SuspiciousOperation('Invalid parameters for `gridftp_upload`!')
336
337     error = None
338     try:
339         FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
340     except FTPError as e:
341         error = e.message
342
343     return render(request, 'qcg/gridftp_upload.html',
344                   {'error': error, 'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
345                    'host': form.cleaned_data['host'], 'path': form.cleaned_data['path'],
346                    'sep': '/' if form.cleaned_data['path'].startswith('~') else ''})
347
348
349 @restricted
350 def job_templates(request):
351     return render(request, 'qcg/job_templates.html', {'templates': request.user.templates.all()})
352
353
354 @require_POST
355 @login_required
356 def template_delete(request, template_id):
357     template = get_object_or_404(request.user.templates, id=template_id)
358     template.delete()
359
360     messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
361                                           u'Usunięto szablon <em>{}</em>.', template.name))
362
363     return redirect('job_templates')
364
365
366 @require_POST
367 @login_required
368 def job_save_template(request, job_id):
369     job = get_object_or_404(request.user.jobs, job_id=job_id)
370
371     template_form = JobTemplateForm(request.POST,
372                                     instance=JobTemplate(owner=request.user, description=job.qcg_description))
373
374     if template_form.is_valid():
375         template = template_form.save()
376
377         return redirect(template)
378
379     messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}'
380                                         'Nie udało się zapisać szablonu!', template_form.errors))
381
382     return redirect(job)