fixed typo
[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 from qcg.utils import paginator_context, to_job_desc, to_form_data
27 from qcg.service import update_user_data, update_job, cancel, clean
28
29
30 from django.utils import encoding
31 import string
32 import random
33 import os
34 from qcg import constants
35 from qcg.utils import chunks
36 # for Debugging
37 from pprint import pprint
38
39 def index(request):
40     return render(request, 'qcg/base.html')
41
42
43 def openid_begin(request):
44     openid_request = make_consumer(request).begin(settings.OPENID_SSO_SERVER_URL)
45
46     fetch_request = ax.FetchRequest()
47     for (attr, alias) in [('http://axschema.org/namePerson/friendly', 'nickname'),
48                           ('http://axschema.org/contact/email', 'email'),
49                           ('http://axschema.org/namePerson', 'fullname'),
50                           ('http://openid.plgrid.pl/certificate/proxy', 'proxy'),
51                           ('http://openid.plgrid.pl/certificate/userCert', 'userCert'),
52                           ('http://openid.plgrid.pl/certificate/proxyPrivKey', 'proxyPrivKey'),
53                           ('http://openid.plgrid.pl/POSTresponse', 'POSTresponse')]:
54         fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True))
55     openid_request.addExtension(fetch_request)
56
57     return_to = request.build_absolute_uri(reverse('openid-complete'))
58
59     redirect_to = request.GET.get(REDIRECT_FIELD_NAME)
60     if redirect_to:
61         return_to += '?' + urlencode({REDIRECT_FIELD_NAME: redirect_to})
62
63     return HttpResponse(openid_request.htmlMarkup(request.build_absolute_uri('/'), return_to))
64
65
66 search_fields = ('status_description', 'type', 'note', 'task_id', 'job__note', 'job__project', 'job__job_id',
67                  'allocations__status_description', 'allocations__processes_group_id', 'allocations__comment')
68
69
70 def parse_date(string):
71     return datetime.strptime(string.strip(), "%d.%m.%Y").replace(tzinfo=UTC())
72
73
74 @login_required
75 def jobs_list(request):
76     update_user_data(request.user, request.session['proxy'])
77
78     tasks = request.user.tasks.order_by('-job__submission_time', '-submission_time') \
79         .select_related('job').prefetch_related('allocations__nodes')
80
81     filters = FiltersForm(request.GET)
82     selected_filters = []
83     if filters.is_valid():
84         keywords = filters.cleaned_data['keywords']
85         status = filters.cleaned_data['status']
86         host = filters.cleaned_data['host']
87         purged = filters.cleaned_data['purged']
88         submission = filters.cleaned_data['submission']
89         finish = filters.cleaned_data['finish']
90
91         if keywords:
92             and_query = Q()
93
94             for q in keywords.split():
95                 or_query = Q()
96                 for field in search_fields:
97                     or_query |= Q(**{field + '__icontains': q})
98                 and_query &= or_query
99
100             tasks = tasks.filter(and_query)
101             selected_filters.append((keywords, 'keywords', keywords))
102
103         if status:
104             statuses = []
105             for s in status:
106                 si = int(s)
107                 statuses.extend(FiltersForm.STATUS_MAP[si])
108                 selected_filters.append((FiltersForm.STATUS_CHOICES_DICT[si], 'status', s))
109
110             tasks = tasks.filter(status__in=statuses)
111
112         if host:
113             tasks = tasks.filter(allocations__host_name__in=host)
114
115             host_dict = dict(filters.fields['host'].choices)
116             for h in host:
117                 selected_filters.append((host_dict[h], 'host', h))
118
119         if isinstance(purged, bool):
120             tasks = tasks.filter(purged=purged)
121
122             if purged:
123                 selected_filters.append((u"Usunięty katalog roboczy", 'purged', '1'))
124             else:
125                 selected_filters.append((u"Istniejący katalog roboczy", 'purged', '0'))
126
127         if submission:
128             start, end = submission.split('-')
129
130             tasks = tasks.filter(submission_time__gte=parse_date(start),
131                                  submission_time__lte=parse_date(end) + timedelta(days=1))
132             selected_filters.append((u'Data zlecenia: ' + submission, 'submission', submission))
133
134         if finish:
135             start, end = finish.split('-')
136
137             tasks = tasks.filter(finish_time__gte=parse_date(start),
138                                  finish_time__lte=parse_date(end) + timedelta(days=1))
139             selected_filters.append((u'Data zakończenia: ' + finish, 'finish', finish))
140
141         tasks = tasks.distinct()
142
143     checked_status = {i: widget.is_checked() for i, widget in enumerate(filters['status'])}
144
145     columns = ColumnsForm(QueryDict(request.COOKIES.get('columns', '')))
146
147     displayed = None
148     if columns.is_valid():
149         displayed = {int(c) for c in columns.cleaned_data['columns']}
150
151     # if invalid or empty
152     if not displayed:
153         displayed = set(columns.fields['columns'].initial)
154
155     context = {'filters': filters, 'checked_status': checked_status, 'selected_filters': selected_filters,
156                'columns': ColumnsForm(initial={'columns': displayed}), 'displayed': displayed}
157
158     context.update(paginator_context(request, tasks))
159
160     return render(request, 'qcg/jobs.html', context)
161
162
163 @login_required
164 def job_details(request, job_id):
165     job = get_object_or_404(request.user.jobs.prefetch_related('tasks'), job_id=job_id)
166
167     return render(request, 'qcg/job.html', process_details(request, job))
168
169
170 @login_required
171 def task_details(request, job_id, task_id):
172     task = get_object_or_404(request.user.tasks.select_related('job').prefetch_related('allocations'),
173                              job__job_id=job_id, task_id=task_id)
174
175     return render(request, 'qcg/task.html', process_details(request, task.job, task))
176
177
178 def process_details(request, job, task=None):
179     if not job.terminated or not job.purged:
180         update_job(job, request.session['proxy'])
181
182     form_data = to_form_data(job.qcg_description)
183     env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
184
185     form = JobDescriptionForm(initial=form_data)
186     env_formset = EnvFormSet(initial=env_formset_data)
187     template_form = JobTemplateForm(initial={'name': (task and task.note) or u'Szablon z {}'.format(job.job_id)})
188
189     for field in form:
190         field.field.widget.attrs['disabled'] = 'True'
191     for env_form in env_formset:
192         for field in env_form:
193             field.field.widget.attrs['disabled'] = 'True'
194
195     return {'job': job, 'task': task, 'form': form, 'env_formset': env_formset, 'template_form': template_form}
196
197 def generate_md_inputfile(params):
198     md_input = list()
199     # Opis pliku wyjsciowego
200     opis=params['note'][:80]
201     md_input.append(encoding.smart_str(opis, encoding='ascii', errors='ignore'))
202     # Dane kontrolne obliczeń
203     md_input.append('SEED=-3059743 PDBREF ONE_LETTER MD EXTCONF RESCALE_MODE=2')
204     ctl_data='nstep='+str(params['nstep'])+' ntwe='+str(params['ntwe'])
205     ctl_data+=' ntwx='+str(params['ntwx'])+' dt='+str(params['dt'])+' damax='+str(params['damax'])+'lang=0 tbf'
206     md_input.append('{:<79}&'.format(ctl_data))
207     md_input.append('tau_bath=1.0 t_bath=300 reset_vel=10000 respa ntime_split=1 maxtime_split=512')
208     # Paramatry pól siłowych
209     if params['force_field'] == 'GAB':
210         # Wagi pola GAB
211         md_input.append('WLONG=1.35279 WSCP=1.59304 WELEC=0.71534 WBOND=1.00000 WANG=1.13873            &')
212         md_input.append('WSCLOC=0.16258 WTOR=1.98599 WTORD=1.57069 WCORRH=0.42887 WCORR5=0.00000        &')
213         md_input.append('WCORR6=0.00000 WEL_LOC=0.16036 WTURN3=1.68722 WTURN4=0.66230 WTURN6=0.00000    &')
214         md_input.append('WVDWPP=0.11371 WHPB=1.00000                                                    &')
215         md_input.append('CUTOFF=7.00000 WCORR4=0.00000 WSCCOR=0.0')
216     else:
217         # Wagi pola E0LLY
218         md_input.append('WLONG=1.00000 WSCP=1.23315 WELEC=0.84476 WBOND=1.00000 WANG=0.62954            &')
219         md_input.append('WSCLOC=0.10554 WTOR=1.84316 WTORD=1.26571 WCORRH=0.19212 WCORR5=0.00000        &')
220         md_input.append('WCORR6=0.00000 WEL_LOC=0.37357 WTURN3=1.40323 WTURN4=0.64673 WTURN6=0.00000    &')
221         md_input.append('WVDWPP=0.23173 WHPB=1.00000 WSCCOR=0.0                                         &')
222         md_input.append('CUTOFF=7.00000 WCORR4=0.00000')
223     # Plik PDB    
224     md_input.append(params['pdb_file'].split('/')[-1])
225     # Sekwencja aminokwasów
226     md_input.append(len(params['sequence']))
227     seq_str=params['sequence']
228     while seq_str:
229         md_input.append(seq_str[:80])
230         seq_str=seq_str[80:]
231     md_input.append(' 0')
232     md_input.append(' 0')
233     
234     return md_input
235
236
237 def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
238     return ''.join(random.choice(chars) for _ in range(size))
239
240 @login_required
241 def job_submit(request, template_id=None):
242     save_template = 'save-template' in request.POST
243     template = get_object_or_404(request.user.templates, id=template_id) if template_id is not None else None
244
245     if template is not None:
246         form_data = to_form_data(template.description)
247         env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
248     else:
249         form_data, env_formset_data = None, None
250
251     if request.method == 'POST':
252         form = JobDescriptionForm(request.POST, initial=form_data)
253         env_formset = EnvFormSet(request.POST, initial=env_formset_data)
254         template_form = JobTemplateForm(request.POST, prefix='template', instance=template)
255
256         if form.is_valid() and env_formset.is_valid() and (not save_template or template_form.is_valid()):
257             params = form.cleaned_data
258             params['env_variables'] = [(env['name'], env['value'])
259                                        for env in env_formset.cleaned_data if env and not env['DELETE']]
260
261             # Generowanie pliku inputowego
262             md_input=generate_md_inputfile(params)
263             url=os.path.splitext(params['pdb_file'])[0]+'_MD_genarated.inp'
264             params['master_file']=url
265             params['stage_in'].append(params['pdb_file'])
266             # Upload
267             ftp = FTPOperation(request.session['proxy'])
268             ftp.put(url)
269             for chunk in chunks('\n'.join([str(i) for i in md_input] ), 4096):
270                 ftp.stream.put(chunk)
271             ftp.stream.put(None)
272             ftp.wait()
273             
274
275             params['persistent'] = True
276             # Debugging parametrów
277             #pprint(params)
278
279             job_desc = to_job_desc(params, request.session['proxy'])
280
281             if save_template:
282                 template = template_form.save(commit=False)
283
284                 template.owner = request.user
285                 template.description = job_desc.xml_description
286                 template.save()
287
288                 return redirect(template)
289             
290             job = job_desc.submit()
291
292             messages.success(request,
293                              format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
294                                          u'Zlecono zadanie <em>{}</em>.', job.job_id))
295             
296             return redirect('jobs')
297     else:
298         form = JobDescriptionForm(initial=form_data)
299         env_formset = EnvFormSet(initial=env_formset_data)
300         template_form = JobTemplateForm(prefix='template', instance=template)
301
302     errors = form.errors or (env_formset.is_bound and not env_formset.is_valid) or (
303         save_template and template_form.errors)
304
305     return render(request, 'qcg/job_submit.html', {'form': form, 'env_formset': env_formset, 'errors': errors,
306                                                    'template_form': template_form, 'template': template})
307
308
309 @require_POST
310 @login_required
311 def job_cancel(request, job_id):
312     return obj_cancel(request, get_object_or_404(request.user.jobs, job_id=job_id))
313
314
315 @require_POST
316 @login_required
317 def task_cancel(request, job_id, task_id):
318     return obj_cancel(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
319
320
321 def obj_cancel(request, obj):
322     try:
323         cancel(obj, request.session['proxy'])
324     except PyqcgException as e:
325         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
326                                             e.message))
327     else:
328         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
329                                               'Zadanie anulowano.'))
330
331     return redirect(obj)
332
333
334 @require_POST
335 @login_required
336 def job_clean(request, job_id):
337     return obj_clean(request, get_object_or_404(request.user.jobs, job_id=job_id))
338
339
340 @require_POST
341 @login_required
342 def task_clean(request, job_id, task_id):
343     return obj_clean(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
344
345
346 def obj_clean(request, obj):
347     try:
348         clean(obj, request.session['proxy'])
349     except PyqcgException as e:
350         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
351                                             e.message))
352     else:
353         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
354                                               'Usunięto katalog roboczy.'))
355
356     return redirect(obj)
357
358
359 @login_required
360 def gridftp(request):
361     return render(request, 'qcg/gridftp.html',
362                   {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(), 'archive_form': ArchiveForm()})
363
364
365 @login_required
366 def gridftp_upload(request):
367     form = HostPathForm(request.GET)
368
369     if not form.is_valid():
370         raise SuspiciousOperation('Invalid parameters for `gridftp_upload`!')
371
372     error = None
373     try:
374         FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
375     except FTPError as e:
376         error = e.message
377
378     return render(request, 'qcg/gridftp_upload.html',
379                   {'error': error, 'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
380                    'host': form.cleaned_data['host'], 'path': form.cleaned_data['path'],
381                    'sep': '/' if form.cleaned_data['path'].startswith('~') else ''})
382
383
384 @login_required
385 def job_templates(request):
386     return render(request, 'qcg/job_templates.html', {'templates': request.user.templates.all()})
387
388
389 @require_POST
390 @login_required
391 def template_delete(request, template_id):
392     template = get_object_or_404(request.user.templates, id=template_id)
393     template.delete()
394
395     messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
396                                           u'Usunięto szablon <em>{}</em>.', template.name))
397
398     return redirect('job_templates')
399
400
401 @require_POST
402 @login_required
403 def job_save_template(request, job_id):
404     job = get_object_or_404(request.user.jobs, job_id=job_id)
405
406     template_form = JobTemplateForm(request.POST,
407                                     instance=JobTemplate(owner=request.user, description=job.qcg_description))
408
409     if template_form.is_valid():
410         template = template_form.save()
411
412         return redirect(template)
413
414     messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}'
415                                         'Nie udało się zapisać szablonu!', template_form.errors))
416
417     return redirect(job)