073c08b4d2515ce958da8b952c8a63c949fe9671
[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, generate_md_inputfile
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
198
199
200 def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
201     return ''.join(random.choice(chars) for _ in range(size))
202
203 @login_required
204 def job_submit(request, template_id=None):
205     save_template = 'save-template' in request.POST
206     template = get_object_or_404(request.user.templates, id=template_id) if template_id is not None else None
207
208     if template is not None:
209         form_data = to_form_data(template.description)
210         env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
211     else:
212         form_data, env_formset_data = None, None
213
214     if request.method == 'POST':
215         form = JobDescriptionForm(request.POST, initial=form_data)
216         env_formset = EnvFormSet(request.POST, initial=env_formset_data)
217         template_form = JobTemplateForm(request.POST, prefix='template', instance=template)
218
219         if form.is_valid() and env_formset.is_valid() and (not save_template or template_form.is_valid()):
220             params = form.cleaned_data
221             params['env_variables'] = [(env['name'], env['value'])
222                                        for env in env_formset.cleaned_data if env and not env['DELETE']]
223
224             # Generowanie pliku inputowego
225             md_input=generate_md_inputfile(params)
226             url=os.path.splitext(params['pdb_file'])[0]+'_MD_genarated.inp'
227             params['master_file']=url
228             params['stage_in'].append(params['pdb_file'])
229             # Upload
230             ftp = FTPOperation(request.session['proxy'])
231             ftp.put(url)
232             for chunk in chunks('\n'.join([str(i) for i in md_input] ), 4096):
233                 ftp.stream.put(chunk)
234             ftp.stream.put(None)
235             ftp.wait()
236             
237             params['persistent'] = True
238             # Debugging parametrów
239             # pprint(params)
240             job_desc = to_job_desc(params, request.session['proxy'])
241
242
243             if save_template:
244                 template = template_form.save(commit=False)
245
246                 template.owner = request.user
247                 template.description = job_desc.xml_description
248                 template.save()
249
250                 return redirect(template)
251
252             # wyłączenie wysyłania zadania - zakomentować poniżej
253             
254             job = job_desc.submit()
255
256             messages.success(request,
257                              format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
258                                          u'Zlecono zadanie <em>{}</em>.', job.job_id))
259             
260             return redirect('jobs')
261     else:
262         form = JobDescriptionForm(initial=form_data)
263         env_formset = EnvFormSet(initial=env_formset_data)
264         template_form = JobTemplateForm(prefix='template', instance=template)
265
266     errors = form.errors or (env_formset.is_bound and not env_formset.is_valid) or (
267         save_template and template_form.errors)
268
269     return render(request, 'qcg/job_submit.html', {'form': form, 'env_formset': env_formset, 'errors': errors,
270                                                    'template_form': template_form, 'template': template})
271
272
273 @require_POST
274 @login_required
275 def job_cancel(request, job_id):
276     return obj_cancel(request, get_object_or_404(request.user.jobs, job_id=job_id))
277
278
279 @require_POST
280 @login_required
281 def task_cancel(request, job_id, task_id):
282     return obj_cancel(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
283
284
285 def obj_cancel(request, obj):
286     try:
287         cancel(obj, request.session['proxy'])
288     except PyqcgException as e:
289         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
290                                             e.message))
291     else:
292         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
293                                               'Zadanie anulowano.'))
294
295     return redirect(obj)
296
297
298 @require_POST
299 @login_required
300 def job_clean(request, job_id):
301     return obj_clean(request, get_object_or_404(request.user.jobs, job_id=job_id))
302
303
304 @require_POST
305 @login_required
306 def task_clean(request, job_id, task_id):
307     return obj_clean(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
308
309
310 def obj_clean(request, obj):
311     try:
312         clean(obj, request.session['proxy'])
313     except PyqcgException as e:
314         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
315                                             e.message))
316     else:
317         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
318                                               'Usunięto katalog roboczy.'))
319
320     return redirect(obj)
321
322
323 @login_required
324 def gridftp(request):
325     return render(request, 'qcg/gridftp.html',
326                   {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(), 'archive_form': ArchiveForm()})
327
328
329 @login_required
330 def gridftp_upload(request):
331     form = HostPathForm(request.GET)
332
333     if not form.is_valid():
334         raise SuspiciousOperation('Invalid parameters for `gridftp_upload`!')
335
336     error = None
337     try:
338         FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
339     except FTPError as e:
340         error = e.message
341
342     return render(request, 'qcg/gridftp_upload.html',
343                   {'error': error, 'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
344                    'host': form.cleaned_data['host'], 'path': form.cleaned_data['path'],
345                    'sep': '/' if form.cleaned_data['path'].startswith('~') else ''})
346
347
348 @login_required
349 def job_templates(request):
350     return render(request, 'qcg/job_templates.html', {'templates': request.user.templates.all()})
351
352
353 @require_POST
354 @login_required
355 def template_delete(request, template_id):
356     template = get_object_or_404(request.user.templates, id=template_id)
357     template.delete()
358
359     messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
360                                           u'Usunięto szablon <em>{}</em>.', template.name))
361
362     return redirect('job_templates')
363
364
365 @require_POST
366 @login_required
367 def job_save_template(request, job_id):
368     job = get_object_or_404(request.user.jobs, job_id=job_id)
369
370     template_form = JobTemplateForm(request.POST,
371                                     instance=JobTemplate(owner=request.user, description=job.qcg_description))
372
373     if template_form.is_valid():
374         template = template_form.save()
375
376         return redirect(template)
377
378     messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}'
379                                         'Nie udało się zapisać szablonu!', template_form.errors))
380
381     return redirect(job)