f498f950ebf78fc6e38ca49778aece940436d658
[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     update_job(job, request.session['proxy'])
159
160     template_form = JobTemplateForm(initial={'name': job.note or u'Szablon z {}'.format(job.job_id)})
161
162     return render(request, 'qcg/job.html', {'job': job, 'template_form': template_form})
163
164
165 @login_required
166 def task_details(request, job_id, task_id):
167     task = get_object_or_404(request.user.tasks.select_related('job').prefetch_related('allocations'),
168                              job__job_id=job_id, task_id=task_id)
169
170     update_job(task.job, request.session['proxy'])
171
172     return render(request, 'qcg/task.html', {'task': task})
173
174
175 @login_required
176 def job_submit(request, template_id=None):
177     save_template = 'save-template' in request.POST
178     template = get_object_or_404(request.user.templates, id=template_id) if template_id is not None else None
179
180     if template is not None:
181         form_data = to_form_data(template.description)
182         env_formset_data = [{'name': name, 'value': value} for name, value in form_data.pop('env_variables', ())]
183     else:
184         form_data, env_formset_data = None, None
185
186     if request.method == 'POST':
187         form = JobDescriptionForm(request.POST, initial=form_data)
188         env_formset = EnvFormSet(request.POST, initial=env_formset_data)
189         template_form = JobTemplateForm(request.POST, prefix='template', instance=template)
190
191         if form.is_valid() and env_formset.is_valid() and (not save_template or template_form.is_valid()):
192             params = form.cleaned_data
193             params['env_variables'] = [(env['name'], env['value'])
194                                        for env in env_formset.cleaned_data if env and not env['DELETE']]
195
196             job_desc = to_job_desc(params, request.session['proxy'])
197
198             if save_template:
199                 template = template_form.save(commit=False)
200
201                 template.owner = request.user
202                 template.description = job_desc.xml_description
203                 template.save()
204
205                 return redirect(template)
206
207             job = job_desc.submit()
208
209             messages.success(request,
210                              format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
211                                          u'Zlecono zadanie <em>{}</em>.', job.job_id))
212
213             return redirect('jobs')
214     else:
215         form = JobDescriptionForm(initial=form_data)
216         env_formset = EnvFormSet(initial=env_formset_data)
217         template_form = JobTemplateForm(prefix='template', instance=template)
218
219     errors = form.errors or (env_formset.is_bound and not env_formset.is_valid) or (
220         save_template and template_form.errors)
221
222     return render(request, 'qcg/job_submit.html', {'form': form, 'env_formset': env_formset, 'errors': errors,
223                                                    'template_form': template_form, 'template': template})
224
225
226 @require_POST
227 @login_required
228 def job_cancel(request, job_id):
229     return obj_cancel(request, get_object_or_404(request.user.jobs, job_id=job_id))
230
231
232 @require_POST
233 @login_required
234 def task_cancel(request, job_id, task_id):
235     return obj_cancel(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
236
237
238 def obj_cancel(request, obj):
239     try:
240         cancel(obj, request.session['proxy'])
241     except PyqcgException as e:
242         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
243                                             e.message))
244     else:
245         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
246                                               'Zadanie anulowano.'))
247
248     return redirect(obj)
249
250
251 @require_POST
252 @login_required
253 def job_clean(request, job_id):
254     return obj_clean(request, get_object_or_404(request.user.jobs, job_id=job_id))
255
256
257 @require_POST
258 @login_required
259 def task_clean(request, job_id, task_id):
260     return obj_clean(request, get_object_or_404(request.user.tasks, job__job_id=job_id, task_id=task_id))
261
262
263 def obj_clean(request, obj):
264     try:
265         clean(obj, request.session['proxy'])
266     except PyqcgException as e:
267         messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}',
268                                             e.message))
269     else:
270         messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
271                                               'Usunięto katalog roboczy.'))
272
273     return redirect(obj)
274
275
276 @login_required
277 def gridftp(request):
278     return render(request, 'qcg/gridftp.html',
279                   {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(), 'archive_form': ArchiveForm()})
280
281
282 @login_required
283 def gridftp_upload(request):
284     form = HostPathForm(request.GET)
285
286     if not form.is_valid():
287         raise SuspiciousOperation('Invalid parameters for `gridftp_upload`!')
288
289     error = None
290     try:
291         FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
292     except FTPError as e:
293         error = e.message
294
295     return render(request, 'qcg/gridftp_upload.html',
296                   {'error': error, 'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
297                    'host': form.cleaned_data['host'], 'path': form.cleaned_data['path'],
298                    'sep': '/' if form.cleaned_data['path'].startswith('~') else ''})
299
300
301 @login_required
302 def job_templates(request):
303     return render(request, 'qcg/job_templates.html', {'templates': request.user.templates.all()})
304
305
306 @require_POST
307 @login_required
308 def template_delete(request, template_id):
309     template = get_object_or_404(request.user.templates, id=template_id)
310     template.delete()
311
312     messages.success(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> '
313                                           u'Usunięto szablon <em>{}</em>.', template.name))
314
315     return redirect('job_templates')
316
317
318 @require_POST
319 @login_required
320 def job_save_template(request, job_id):
321     job = get_object_or_404(request.user.jobs, job_id=job_id)
322
323     template_form = JobTemplateForm(request.POST,
324                                     instance=JobTemplate(owner=request.user, description=job.qcg_description))
325
326     if template_form.is_valid():
327         template = template_form.save()
328
329         return redirect(template)
330
331     messages.error(request, format_html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {}'
332                                         'Nie udało się zapisać szablonu!', template_form.errors))
333
334     return redirect(job)