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