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