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