File indexing completed on 2026-04-25 08:29:11
0001 """
0002 PCS web UI views and DataTable AJAX endpoints.
0003
0004 Views are generic across tag types (p/e/s/r) where possible, parameterized by tag_type.
0005 Tag list views use server-side DataTables via monitor_app._datatable_base.html.
0006 Read operations are public; create/edit/lock require login.
0007 """
0008 import json
0009 from urllib.parse import quote as urlquote
0010 from django.shortcuts import render, get_object_or_404, redirect
0011 from django.contrib.auth.decorators import login_required
0012 from django.urls import reverse
0013 from django.http import JsonResponse
0014 from django.contrib import messages
0015 from django.db.models import Count
0016
0017 from monitor_app.utils import DataTablesProcessor, get_filter_params, format_datetime
0018
0019 from .models import (
0020 PhysicsCategory, PhysicsTag, EvgenTag, SimuTag, RecoTag,
0021 Dataset, ProdConfig, ProdTask,
0022 )
0023 from .schemas import TAG_SCHEMAS, get_tag_model, get_param_defs, save_param_defs
0024 from .forms import PhysicsTagForm, SimpleTagForm, DatasetForm, PhysicsCategoryForm, ProdConfigForm
0025
0026
0027 def pcs_hub_counts():
0028 """PCS entity counts — shared by PCS hub and production hub."""
0029 return {
0030 'categories_count': PhysicsCategory.objects.count(),
0031 'physics_tags_count': PhysicsTag.objects.count(),
0032 'evgen_tags_count': EvgenTag.objects.count(),
0033 'simu_tags_count': SimuTag.objects.count(),
0034 'reco_tags_count': RecoTag.objects.count(),
0035 'datasets_count': Dataset.objects.values('dataset_name').distinct().count(),
0036 'prod_configs_count': ProdConfig.objects.count(),
0037 'prod_tasks_count': ProdTask.objects.count(),
0038 }
0039
0040
0041 def pcs_hub(request):
0042 return render(request, 'pcs/pcs_hub.html', pcs_hub_counts())
0043
0044
0045
0046
0047 def physics_categories_list(request):
0048 categories = PhysicsCategory.objects.annotate(tag_count=Count('tags'))
0049 return render(request, 'pcs/physics_categories_list.html', {'categories': categories})
0050
0051
0052 @login_required
0053 def physics_category_create(request):
0054 if request.method == 'POST':
0055 form = PhysicsCategoryForm(request.POST)
0056 if form.is_valid():
0057 form.save()
0058 messages.success(request, f"Category {form.instance.digit}: {form.instance.name} created.")
0059 return redirect('pcs:physics_categories_list')
0060 else:
0061 form = PhysicsCategoryForm()
0062 return render(request, 'pcs/physics_category_create.html', {'form': form})
0063
0064
0065
0066
0067 TAG_MODELS = {
0068 'p': PhysicsTag,
0069 'e': EvgenTag,
0070 's': SimuTag,
0071 'r': RecoTag,
0072 }
0073
0074
0075 def tags_list(request, tag_type):
0076 schema = TAG_SCHEMAS[tag_type]
0077 model = TAG_MODELS[tag_type]
0078
0079 status_filter = request.GET.get('status', '')
0080 category_filter = request.GET.get('category', '')
0081
0082 columns = [
0083 {'name': 'tag_label', 'title': 'Tag', 'orderable': True},
0084 {'name': 'description', 'title': 'Description', 'orderable': True},
0085 {'name': 'status', 'title': 'Status', 'orderable': True},
0086 {'name': 'created_by', 'title': 'Created By', 'orderable': True},
0087 {'name': 'created_at', 'title': 'Created', 'orderable': True},
0088 {'name': 'actions', 'title': '', 'orderable': False},
0089 ]
0090 if tag_type == 'p':
0091 columns.insert(1, {'name': 'category__name', 'title': 'Category', 'orderable': True})
0092
0093 statuses = ['draft', 'locked']
0094 categories = list(PhysicsCategory.objects.values_list('name', flat=True)) if tag_type == 'p' else []
0095
0096 context = {
0097 'table_title': f'{schema["label"]} Tags',
0098 'table_description': f'All {schema["label"].lower()} tags registered in PCS.',
0099 'ajax_url': reverse('pcs:tags_datatable_ajax', args=[tag_type]),
0100 'columns': columns,
0101 'tag_type': tag_type,
0102 'schema': schema,
0103 'statuses': statuses,
0104 'categories': categories,
0105 'selected_status': status_filter,
0106 'selected_category': category_filter,
0107 }
0108 return render(request, 'pcs/tag_list.html', context)
0109
0110
0111 def tags_datatable_ajax(request, tag_type):
0112 model = TAG_MODELS[tag_type]
0113
0114 if tag_type == 'p':
0115 col_names = ['tag_label', 'category__name', 'description', 'status', 'created_by', 'created_at', 'actions']
0116 else:
0117 col_names = ['tag_label', 'description', 'status', 'created_by', 'created_at', 'actions']
0118
0119 dt = DataTablesProcessor(request, col_names, default_order_column=0, default_order_direction='desc')
0120
0121 qs = model.objects.all()
0122 if tag_type == 'p':
0123 qs = qs.select_related('category')
0124
0125 filters = get_filter_params(request, ['status', 'category'])
0126 if filters['status']:
0127 qs = qs.filter(status=filters['status'])
0128 if tag_type == 'p' and filters.get('category'):
0129 qs = qs.filter(category__name=filters['category'])
0130
0131 records_total = model.objects.count()
0132 search_fields = ['tag_label', 'description', 'created_by']
0133 if tag_type == 'p':
0134 search_fields.append('category__name')
0135 qs = dt.apply_search(qs, search_fields)
0136 records_filtered = qs.count()
0137
0138 qs = qs.order_by(dt.get_order_by())
0139 page = dt.apply_pagination(qs)
0140
0141 data = []
0142 for tag in page:
0143 compose_url = reverse('pcs:tag_compose', args=[tag_type])
0144 tag_url = f'{compose_url}?selected={urlquote(tag.tag_label)}'
0145 tag_link = f'<a href="{tag_url}">{tag.tag_label}</a>'
0146 status_badge = (
0147 f'<span class="badge bg-secondary">{tag.status}</span>'
0148 if tag.status == 'draft'
0149 else f'<span class="badge bg-success">{tag.status}</span>'
0150 )
0151 row = [tag_link]
0152 if tag_type == 'p':
0153 row.append(tag.category.name)
0154 row += [
0155 tag.description[:80] + ('...' if len(tag.description) > 80 else ''),
0156 status_badge,
0157 tag.created_by,
0158 format_datetime(tag.created_at),
0159 f'<a href="{tag_url}">View</a>',
0160 ]
0161 data.append(row)
0162
0163 return dt.create_response(data, records_total, records_filtered)
0164
0165
0166 def tag_detail(request, tag_type, tag_number):
0167 model = TAG_MODELS[tag_type]
0168 schema = TAG_SCHEMAS[tag_type]
0169 tag = get_object_or_404(model, tag_number=tag_number)
0170
0171 datasets = []
0172 if tag.status == 'locked':
0173 filter_kwarg = {f'{schema["prefix"]}__tag_number' if schema["prefix"] == 'p' else f'{"physics" if schema["prefix"] == "p" else {"e": "evgen", "s": "simu", "r": "reco"}[schema["prefix"]]}_tag': tag}
0174
0175 field_map = {'p': 'physics_tag', 'e': 'evgen_tag', 's': 'simu_tag', 'r': 'reco_tag'}
0176 datasets = Dataset.objects.filter(**{field_map[tag_type]: tag}).order_by('-created_at')
0177
0178 defs = get_param_defs(tag_type)
0179 context = {
0180 'tag': tag,
0181 'tag_type': tag_type,
0182 'schema': schema,
0183 'datasets': datasets,
0184 'required_fields': [d['name'] for d in defs if d.get('required')],
0185 'optional_fields': [d['name'] for d in defs if not d.get('required')],
0186 }
0187 return render(request, 'pcs/tag_detail.html', context)
0188
0189
0190 @login_required
0191 def tag_create(request, tag_type):
0192 schema = TAG_SCHEMAS[tag_type]
0193
0194 if tag_type == 'p':
0195 FormClass = PhysicsTagForm
0196 form_kwargs = {}
0197 else:
0198 FormClass = SimpleTagForm
0199 form_kwargs = {'tag_type': tag_type}
0200
0201 if request.method == 'POST':
0202 form = FormClass(request.POST, **form_kwargs)
0203 if form.is_valid():
0204 model = TAG_MODELS[tag_type]
0205 params = form.get_parameters()
0206
0207 if tag_type == 'p':
0208 category = form.cleaned_data['category']
0209 tag_number = PhysicsTag.allocate_next(category)
0210 tag = PhysicsTag(
0211 tag_number=tag_number,
0212 category=category,
0213 description=form.cleaned_data['description'],
0214 parameters=params,
0215 created_by=form.cleaned_data['created_by'],
0216 )
0217 else:
0218 tag_number = model.allocate_next()
0219 tag = model(
0220 tag_number=tag_number,
0221 description=form.cleaned_data['description'],
0222 parameters=params,
0223 created_by=form.cleaned_data['created_by'],
0224 )
0225 tag.save()
0226 messages.success(request, f"Tag {tag.tag_label} created.")
0227 compose_url = reverse('pcs:tag_compose', kwargs={'tag_type': tag_type})
0228 return redirect(f'{compose_url}?selected={urlquote(tag.tag_label)}')
0229 else:
0230 form = FormClass(**form_kwargs)
0231
0232 context = {
0233 'form': form,
0234 'tag_type': tag_type,
0235 'schema': schema,
0236 }
0237 template = 'pcs/tag_create_physics.html' if tag_type == 'p' else 'pcs/tag_create.html'
0238 return render(request, template, context)
0239
0240
0241 def tag_compose(request, tag_type):
0242 """Split-panel browse + compose UI for physics tags."""
0243 schema = TAG_SCHEMAS[tag_type]
0244 model = TAG_MODELS[tag_type]
0245
0246 if tag_type == 'p':
0247 FormClass = PhysicsTagForm
0248 form_kwargs = {}
0249 else:
0250 FormClass = SimpleTagForm
0251 form_kwargs = {'tag_type': tag_type}
0252
0253 selected_tag = None
0254 if request.method == 'POST':
0255 if not request.user.is_authenticated:
0256 from django.contrib.auth.views import redirect_to_login
0257 return redirect_to_login(request.get_full_path())
0258 form = FormClass(request.POST, **form_kwargs)
0259 if form.is_valid():
0260 params = form.get_parameters()
0261 if tag_type == 'p':
0262 category = form.cleaned_data['category']
0263 tag_number = PhysicsTag.allocate_next(category)
0264 tag = PhysicsTag(
0265 tag_number=tag_number,
0266 category=category,
0267 description=form.cleaned_data['description'],
0268 parameters=params,
0269 created_by=form.cleaned_data['created_by'],
0270 )
0271 else:
0272 tag_number = model.allocate_next()
0273 tag = model(
0274 tag_number=tag_number,
0275 description=form.cleaned_data['description'],
0276 parameters=params,
0277 created_by=form.cleaned_data['created_by'],
0278 )
0279 tag.save()
0280 messages.success(request, f"Tag {tag.tag_label} created.")
0281 compose_url = reverse('pcs:tag_compose', kwargs={'tag_type': tag_type})
0282 return redirect(f'{compose_url}?selected={urlquote(tag.tag_label)}')
0283 else:
0284 form = FormClass(**form_kwargs)
0285 selected_tag = request.GET.get('selected')
0286
0287 qs = model.objects.order_by('-tag_number')
0288 if tag_type == 'p':
0289 qs = qs.select_related('category')
0290 tags_data = []
0291 for t in qs:
0292 entry = {
0293 'tag_number': t.tag_number,
0294 'tag_label': t.tag_label,
0295 'status': t.status,
0296 'description': t.description,
0297 'parameters': t.parameters,
0298 'created_by': t.created_by,
0299 'created_at': t.created_at.strftime('%Y-%m-%d %H:%M'),
0300 }
0301 if tag_type == 'p':
0302 entry['category_digit'] = t.category.digit
0303 entry['category_name'] = t.category.name
0304 tags_data.append(entry)
0305
0306 param_defs = get_param_defs(tag_type)
0307 choices_from_defs = {d['name']: d['choices'] for d in param_defs if d.get('choices')}
0308 filter_fields = [d['name'] for d in param_defs
0309 if d['name'] not in ('notes', 'description')]
0310
0311
0312 from monitor_app.models import PersistentState
0313 state_keys = {'p': 'pcs_next_physics', 'e': 'pcs_next_evgen',
0314 's': 'pcs_next_simu', 'r': 'pcs_next_reco'}
0315 try:
0316 ps = PersistentState.objects.get(id=1)
0317 next_suffix = ps.state_data.get(state_keys[tag_type], 1)
0318 except PersistentState.DoesNotExist:
0319 next_suffix = 1
0320
0321 context = {
0322 'form': form,
0323 'tag_type': tag_type,
0324 'schema': schema,
0325 'tags_json': json.dumps(tags_data, default=str),
0326 'choices_json': json.dumps(choices_from_defs),
0327 'filter_fields_json': json.dumps(filter_fields),
0328 'param_defs_json': json.dumps(param_defs),
0329 'next_suffix': next_suffix,
0330 'username': request.user.username if request.user.is_authenticated else '',
0331 'selected_tag_json': json.dumps(selected_tag),
0332 }
0333 return render(request, 'pcs/tag_compose.html', context)
0334
0335
0336 def param_defs_api(request, tag_type):
0337 if tag_type not in TAG_SCHEMAS:
0338 return JsonResponse({'error': 'Invalid tag type'}, status=400)
0339 if request.method == 'GET':
0340 return JsonResponse({'defs': get_param_defs(tag_type)})
0341 if not request.user.is_authenticated:
0342 return JsonResponse({'error': 'Login required'}, status=403)
0343 if request.method == 'POST':
0344 try:
0345 data = json.loads(request.body)
0346 except json.JSONDecodeError:
0347 return JsonResponse({'error': 'Invalid JSON'}, status=400)
0348 defs = data.get('defs')
0349 if not isinstance(defs, list):
0350 return JsonResponse({'error': 'defs must be a list'}, status=400)
0351 names_seen = set()
0352 for i, d in enumerate(defs):
0353 if not isinstance(d, dict) or not d.get('name'):
0354 return JsonResponse({'error': f'Invalid param def at index {i}'}, status=400)
0355 name = d['name'].strip()
0356 if name in names_seen:
0357 return JsonResponse({'error': f'Duplicate param name: {name}'}, status=400)
0358 names_seen.add(name)
0359 d['name'] = name
0360 d.setdefault('type', 'string')
0361 d.setdefault('required', False)
0362 d.setdefault('choices', [])
0363 d.setdefault('allow_other', True)
0364 d['sort_order'] = i
0365 save_param_defs(tag_type, defs)
0366 return JsonResponse({'ok': True, 'defs': defs})
0367 return JsonResponse({'error': 'Method not allowed'}, status=405)
0368
0369
0370 @login_required
0371 def tag_delete(request, tag_type, tag_number):
0372 if request.method != 'POST':
0373 return redirect('pcs:tag_compose', tag_type=tag_type)
0374 model = TAG_MODELS[tag_type]
0375 tag = get_object_or_404(model, tag_number=tag_number)
0376 if tag.status == 'locked':
0377 messages.error(request, f"Tag {tag.tag_label} is locked and cannot be deleted.")
0378 return redirect('pcs:tag_compose', tag_type=tag_type)
0379 if tag.created_by != request.user.username:
0380 messages.error(request, f"Only the creator ({tag.created_by}) can delete {tag.tag_label}.")
0381 return redirect('pcs:tag_compose', tag_type=tag_type)
0382 label = tag.tag_label
0383 tag.delete()
0384 messages.success(request, f"Tag {label} deleted.")
0385 return redirect('pcs:tag_compose', tag_type=tag_type)
0386
0387
0388 @login_required
0389 def tag_lock(request, tag_type, tag_number):
0390 compose_url = reverse('pcs:tag_compose', kwargs={'tag_type': tag_type})
0391 selected_url = f'{compose_url}?selected={tag_number}'
0392 if request.method != 'POST':
0393 return redirect(selected_url)
0394 model = TAG_MODELS[tag_type]
0395 tag = get_object_or_404(model, tag_number=tag_number)
0396 if tag.created_by != request.user.username:
0397 messages.error(request, f"Only the creator ({tag.created_by}) can lock this tag.")
0398 elif tag.status == 'locked':
0399 messages.warning(request, f"Tag {tag.tag_label} is already locked.")
0400 else:
0401 tag.status = 'locked'
0402 tag.save(update_fields=['status', 'updated_at'])
0403 messages.success(request, f"Tag {tag.tag_label} locked. It can now be used in datasets.")
0404 return redirect(selected_url)
0405
0406
0407 @login_required
0408 def tag_edit(request, tag_type, tag_number):
0409 model = TAG_MODELS[tag_type]
0410 schema = TAG_SCHEMAS[tag_type]
0411 tag = get_object_or_404(model, tag_number=tag_number)
0412
0413 compose_url = reverse('pcs:tag_compose', kwargs={'tag_type': tag_type})
0414 selected_url = f'{compose_url}?selected={tag_number}'
0415 if tag.status == 'locked':
0416 messages.error(request, f"Tag {tag.tag_label} is locked and cannot be edited.")
0417 return redirect(selected_url)
0418
0419 if tag_type == 'p':
0420 FormClass = PhysicsTagForm
0421 form_kwargs = {}
0422 else:
0423 FormClass = SimpleTagForm
0424 form_kwargs = {'tag_type': tag_type}
0425
0426 if request.method == 'POST':
0427 form = FormClass(request.POST, **form_kwargs)
0428 if form.is_valid():
0429 tag.description = form.cleaned_data['description']
0430 tag.parameters = form.get_parameters()
0431 if tag_type == 'p':
0432 tag.category = form.cleaned_data['category']
0433 tag.save()
0434 messages.success(request, f"Tag {tag.tag_label} updated.")
0435 return redirect(selected_url)
0436 else:
0437 initial = {
0438 'description': tag.description,
0439 'created_by': tag.created_by,
0440 }
0441 if tag_type == 'p':
0442 initial['category'] = tag.category
0443 for k, v in tag.parameters.items():
0444 initial[f'param_{k}'] = v
0445 form = FormClass(initial=initial, **form_kwargs)
0446
0447 context = {
0448 'form': form,
0449 'tag': tag,
0450 'tag_type': tag_type,
0451 'schema': schema,
0452 'editing': True,
0453 }
0454 template = 'pcs/tag_create_physics.html' if tag_type == 'p' else 'pcs/tag_create.html'
0455 return render(request, template, context)
0456
0457
0458
0459
0460 def datasets_compose(request):
0461 """Two-pane browse/create UI for datasets."""
0462 if request.method == 'POST' and request.user.is_authenticated:
0463 form = DatasetForm(request.POST)
0464 if form.is_valid():
0465 cd = form.cleaned_data
0466 ds = Dataset(
0467 scope=cd['scope'],
0468 detector_version=cd['detector_version'],
0469 detector_config=cd['detector_config'],
0470 physics_tag=cd['physics_tag'],
0471 evgen_tag=cd['evgen_tag'],
0472 simu_tag=cd['simu_tag'],
0473 reco_tag=cd['reco_tag'],
0474 description=cd.get('description', ''),
0475 created_by=cd['created_by'],
0476 )
0477 ds.save()
0478 messages.success(request, f"Dataset created: {ds.did}")
0479 return redirect(f"{reverse('pcs:datasets_compose')}?selected={urlquote(ds.dataset_name)}")
0480
0481 qs = Dataset.objects.filter(block_num=1).select_related(
0482 'physics_tag', 'evgen_tag', 'simu_tag', 'reco_tag',
0483 ).order_by('-created_at')
0484 datasets_data = []
0485 for ds in qs:
0486 datasets_data.append({
0487 'id': ds.id,
0488 'dataset_name': ds.dataset_name,
0489 'did': ds.did,
0490 'scope': ds.scope,
0491 'detector_version': ds.detector_version,
0492 'detector_config': ds.detector_config,
0493 'description': ds.description,
0494 'blocks': ds.blocks,
0495 'created_by': ds.created_by,
0496 'created_at': ds.created_at.strftime('%Y-%m-%d %H:%M'),
0497 'physics_tag': {'id': ds.physics_tag_id, 'label': ds.physics_tag.tag_label,
0498 'description': ds.physics_tag.description, 'parameters': ds.physics_tag.parameters},
0499 'evgen_tag': {'id': ds.evgen_tag_id, 'label': ds.evgen_tag.tag_label,
0500 'description': ds.evgen_tag.description, 'parameters': ds.evgen_tag.parameters},
0501 'simu_tag': {'id': ds.simu_tag_id, 'label': ds.simu_tag.tag_label,
0502 'description': ds.simu_tag.description, 'parameters': ds.simu_tag.parameters},
0503 'reco_tag': {'id': ds.reco_tag_id, 'label': ds.reco_tag.tag_label,
0504 'description': ds.reco_tag.description, 'parameters': ds.reco_tag.parameters},
0505 })
0506
0507
0508 tags_data = {}
0509 for ttype, model in TAG_MODELS_MAP.items():
0510 tag_list = []
0511 qs_tags = model.objects.order_by('tag_number')
0512 if ttype == 'p':
0513 qs_tags = qs_tags.select_related('category')
0514 for t in qs_tags:
0515 entry = {'id': t.id, 'tag_number': t.tag_number, 'label': t.tag_label,
0516 'description': t.description, 'status': t.status,
0517 'parameters': t.parameters, 'created_by': t.created_by}
0518 if ttype == 'p':
0519 entry['category_name'] = t.category.name
0520 tag_list.append(entry)
0521 tags_data[ttype] = tag_list
0522
0523 context = {
0524 'datasets_json': json.dumps(datasets_data),
0525 'tags_json': json.dumps(tags_data),
0526 'selected_item_json': json.dumps(request.GET.get('selected') or None),
0527 'username': request.user.username if request.user.is_authenticated else '',
0528 }
0529 return render(request, 'pcs/dataset_compose.html', context)
0530
0531
0532 def datasets_list(request):
0533 columns = [
0534 {'name': 'dataset_name', 'title': 'Dataset Name', 'orderable': True},
0535 {'name': 'physics_tag__tag_label', 'title': 'Physics', 'orderable': True},
0536 {'name': 'evgen_tag__tag_label', 'title': 'EvGen', 'orderable': True},
0537 {'name': 'simu_tag__tag_label', 'title': 'Simu', 'orderable': True},
0538 {'name': 'reco_tag__tag_label', 'title': 'Reco', 'orderable': True},
0539 {'name': 'blocks', 'title': 'Blocks', 'orderable': True},
0540 {'name': 'created_at', 'title': 'Created', 'orderable': True},
0541 ]
0542 context = {
0543 'table_title': 'Datasets',
0544 'table_description': 'All datasets registered in PCS.',
0545 'ajax_url': reverse('pcs:datasets_datatable_ajax'),
0546 'columns': columns,
0547 }
0548 return render(request, 'pcs/datasets_list.html', context)
0549
0550
0551 def datasets_datatable_ajax(request):
0552 col_names = [
0553 'dataset_name', 'physics_tag__tag_label', 'evgen_tag__tag_label',
0554 'simu_tag__tag_label', 'reco_tag__tag_label', 'blocks', 'created_at',
0555 ]
0556 dt = DataTablesProcessor(request, col_names, default_order_column=6, default_order_direction='desc')
0557
0558
0559 qs = Dataset.objects.filter(block_num=1).select_related(
0560 'physics_tag', 'evgen_tag', 'simu_tag', 'reco_tag'
0561 )
0562
0563 records_total = Dataset.objects.filter(block_num=1).count()
0564 search_fields = ['dataset_name', 'physics_tag__tag_label', 'evgen_tag__tag_label',
0565 'simu_tag__tag_label', 'reco_tag__tag_label']
0566 qs = dt.apply_search(qs, search_fields)
0567 records_filtered = qs.count()
0568 qs = qs.order_by(dt.get_order_by())
0569 page = dt.apply_pagination(qs)
0570
0571 data = []
0572 for ds in page:
0573 detail_url = reverse('pcs:dataset_detail', args=[ds.id])
0574 p_url = f"{reverse('pcs:tag_compose', args=['p'])}?selected={ds.physics_tag.tag_number}"
0575 e_url = f"{reverse('pcs:tag_compose', args=['e'])}?selected={ds.evgen_tag.tag_number}"
0576 s_url = f"{reverse('pcs:tag_compose', args=['s'])}?selected={ds.simu_tag.tag_number}"
0577 r_url = f"{reverse('pcs:tag_compose', args=['r'])}?selected={ds.reco_tag.tag_number}"
0578 data.append([
0579 f'<a href="{detail_url}">{ds.dataset_name}</a>',
0580 f'<a href="{p_url}" title="{ds.physics_tag.description}">{ds.physics_tag.tag_label}</a>',
0581 f'<a href="{e_url}" title="{ds.evgen_tag.description}">{ds.evgen_tag.tag_label}</a>',
0582 f'<a href="{s_url}" title="{ds.simu_tag.description}">{ds.simu_tag.tag_label}</a>',
0583 f'<a href="{r_url}" title="{ds.reco_tag.description}">{ds.reco_tag.tag_label}</a>',
0584 str(ds.blocks),
0585 format_datetime(ds.created_at),
0586 ])
0587
0588 return dt.create_response(data, records_total, records_filtered)
0589
0590
0591 def dataset_detail(request, pk):
0592 dataset = get_object_or_404(
0593 Dataset.objects.select_related('physics_tag', 'evgen_tag', 'simu_tag', 'reco_tag'),
0594 pk=pk,
0595 )
0596 blocks = Dataset.objects.filter(dataset_name=dataset.dataset_name).order_by('block_num')
0597 context = {
0598 'dataset': dataset,
0599 'blocks': blocks,
0600 }
0601 return render(request, 'pcs/dataset_detail.html', context)
0602
0603
0604 @login_required
0605 def dataset_create(request):
0606 if request.method == 'POST':
0607 form = DatasetForm(request.POST)
0608 if form.is_valid():
0609 cd = form.cleaned_data
0610 ds = Dataset(
0611 scope=cd['scope'],
0612 detector_version=cd['detector_version'],
0613 detector_config=cd['detector_config'],
0614 physics_tag=cd['physics_tag'],
0615 evgen_tag=cd['evgen_tag'],
0616 simu_tag=cd['simu_tag'],
0617 reco_tag=cd['reco_tag'],
0618 description=cd.get('description', ''),
0619 created_by=cd['created_by'],
0620 )
0621 ds.save()
0622 messages.success(request, f"Dataset created: {ds.did}")
0623 return redirect('pcs:dataset_detail', pk=ds.pk)
0624 else:
0625 form = DatasetForm()
0626 return render(request, 'pcs/dataset_create.html', {'form': form})
0627
0628
0629 @login_required
0630 def dataset_add_block(request, pk):
0631 if request.method != 'POST':
0632 return redirect('pcs:dataset_detail', pk=pk)
0633 dataset = get_object_or_404(Dataset, pk=pk)
0634 new_block_num = dataset.blocks + 1
0635 Dataset.objects.filter(dataset_name=dataset.dataset_name).update(blocks=new_block_num)
0636 new_block = Dataset.objects.create(
0637 dataset_name=dataset.dataset_name,
0638 scope=dataset.scope,
0639 detector_version=dataset.detector_version,
0640 detector_config=dataset.detector_config,
0641 physics_tag=dataset.physics_tag,
0642 evgen_tag=dataset.evgen_tag,
0643 simu_tag=dataset.simu_tag,
0644 reco_tag=dataset.reco_tag,
0645 block_num=new_block_num,
0646 blocks=new_block_num,
0647 did=f"{dataset.scope}:{dataset.dataset_name}.b{new_block_num}",
0648 description=dataset.description,
0649 metadata=dataset.metadata,
0650 created_by=request.user.username if request.user.is_authenticated else 'unknown',
0651 )
0652 messages.success(request, f"Block {new_block_num} added: {new_block.did}")
0653 return redirect('pcs:dataset_detail', pk=dataset.pk)
0654
0655
0656
0657
0658 def prod_configs_compose(request):
0659 """Two-pane browse/create/edit UI for production configs."""
0660 if request.method == 'POST' and request.user.is_authenticated:
0661 editing_pk = request.POST.get('editing_pk')
0662 if editing_pk:
0663 instance = get_object_or_404(ProdConfig, pk=editing_pk)
0664 form = ProdConfigForm(request.POST, instance=instance)
0665 else:
0666 form = ProdConfigForm(request.POST)
0667 if form.is_valid():
0668 pc = form.save()
0669 messages.success(request, f"Config '{pc.name}' {'updated' if editing_pk else 'created'}.")
0670 return redirect(f"{reverse('pcs:prod_configs_compose')}?selected={urlquote(pc.name)}")
0671
0672 qs = ProdConfig.objects.order_by('-updated_at')
0673 configs_data = []
0674 for pc in qs:
0675 configs_data.append({
0676 'id': pc.id,
0677 'name': pc.name,
0678 'description': pc.description,
0679 'bg_mixing': pc.bg_mixing,
0680 'bg_cross_section': pc.bg_cross_section,
0681 'bg_evtgen_file': pc.bg_evtgen_file,
0682 'copy_reco': pc.copy_reco,
0683 'copy_full': pc.copy_full,
0684 'copy_log': pc.copy_log,
0685 'use_rucio': pc.use_rucio,
0686 'jug_xl_tag': pc.jug_xl_tag,
0687 'container_image': pc.container_image,
0688 'target_hours_per_job': str(pc.target_hours_per_job) if pc.target_hours_per_job else '',
0689 'events_per_task': pc.events_per_task,
0690 'panda_site': pc.panda_site,
0691 'panda_queue': pc.panda_queue,
0692 'panda_working_group': pc.panda_working_group,
0693 'panda_resource_type': pc.panda_resource_type,
0694 'rucio_rse': pc.rucio_rse,
0695 'rucio_replication_rules': pc.rucio_replication_rules,
0696 'condor_template': pc.condor_template,
0697 'data': pc.data or {},
0698 'created_by': pc.created_by,
0699 'created_at': pc.created_at.strftime('%Y-%m-%d %H:%M'),
0700 'updated_at': pc.updated_at.strftime('%Y-%m-%d %H:%M'),
0701 })
0702
0703 context = {
0704 'configs_json': json.dumps(configs_data),
0705 'selected_item_json': json.dumps(request.GET.get('selected') or None),
0706 'username': request.user.username if request.user.is_authenticated else '',
0707 }
0708 return render(request, 'pcs/prod_config_compose.html', context)
0709
0710
0711 def prod_configs_list(request):
0712 columns = [
0713 {'name': 'name', 'title': 'Name', 'orderable': True},
0714 {'name': 'description', 'title': 'Description', 'orderable': True},
0715 {'name': 'jug_xl_tag', 'title': 'JUG_XL', 'orderable': True},
0716 {'name': 'target_hours_per_job', 'title': 'Hours/Job', 'orderable': True},
0717 {'name': 'events_per_task', 'title': 'Events/Task', 'orderable': True},
0718 {'name': 'created_by', 'title': 'Created By', 'orderable': True},
0719 {'name': 'updated_at', 'title': 'Updated', 'orderable': True},
0720 ]
0721 context = {
0722 'table_title': 'Production Configs',
0723 'table_description': 'Reusable production configuration templates for job submission.',
0724 'ajax_url': reverse('pcs:prod_configs_datatable_ajax'),
0725 'columns': columns,
0726 }
0727 return render(request, 'pcs/prod_configs_list.html', context)
0728
0729
0730 def prod_configs_datatable_ajax(request):
0731 col_names = ['name', 'description', 'jug_xl_tag', 'target_hours_per_job',
0732 'events_per_task', 'created_by', 'updated_at']
0733 dt = DataTablesProcessor(request, col_names, default_order_column=6, default_order_direction='desc')
0734
0735 qs = ProdConfig.objects.all()
0736 records_total = qs.count()
0737 search_fields = ['name', 'description', 'created_by', 'jug_xl_tag']
0738 qs = dt.apply_search(qs, search_fields)
0739 records_filtered = qs.count()
0740 qs = qs.order_by(dt.get_order_by())
0741 page = dt.apply_pagination(qs)
0742
0743 data = []
0744 for pc in page:
0745 detail_url = reverse('pcs:prod_config_detail', args=[pc.pk])
0746 data.append([
0747 f'<a href="{detail_url}">{pc.name}</a>',
0748 pc.description[:80] + ('...' if len(pc.description) > 80 else ''),
0749 pc.jug_xl_tag or '-',
0750 str(pc.target_hours_per_job) if pc.target_hours_per_job else '-',
0751 str(pc.events_per_task) if pc.events_per_task else '-',
0752 pc.created_by,
0753 format_datetime(pc.updated_at),
0754 ])
0755
0756 return dt.create_response(data, records_total, records_filtered)
0757
0758
0759 def prod_config_detail(request, pk):
0760 config = get_object_or_404(ProdConfig, pk=pk)
0761 return render(request, 'pcs/prod_config_detail.html', {'config': config})
0762
0763
0764 @login_required
0765 def prod_config_create(request):
0766 if request.method == 'POST':
0767 form = ProdConfigForm(request.POST)
0768 if form.is_valid():
0769 form.save()
0770 messages.success(request, f"Production config '{form.instance.name}' created.")
0771 return redirect('pcs:prod_config_detail', pk=form.instance.pk)
0772 else:
0773 form = ProdConfigForm()
0774 return render(request, 'pcs/prod_config_form.html', {'form': form})
0775
0776
0777 @login_required
0778 def prod_config_edit(request, pk):
0779 config = get_object_or_404(ProdConfig, pk=pk)
0780 if request.method == 'POST':
0781 form = ProdConfigForm(request.POST, instance=config)
0782 if form.is_valid():
0783 form.save()
0784 messages.success(request, f"Production config '{config.name}' updated.")
0785 return redirect('pcs:prod_config_detail', pk=config.pk)
0786 else:
0787 form = ProdConfigForm(instance=config)
0788 return render(request, 'pcs/prod_config_form.html', {'form': form, 'editing': True, 'config': config})
0789
0790
0791
0792
0793 TAG_MODELS_MAP = {'p': PhysicsTag, 'e': EvgenTag, 's': SimuTag, 'r': RecoTag}
0794
0795
0796 def prod_tasks_list(request):
0797 columns = [
0798 {'name': 'name', 'title': 'Name', 'orderable': True},
0799 {'name': 'status', 'title': 'Status', 'orderable': True},
0800 {'name': 'dataset__dataset_name', 'title': 'Dataset', 'orderable': True},
0801 {'name': 'prod_config__name', 'title': 'Config', 'orderable': True},
0802 {'name': 'created_by', 'title': 'Created By', 'orderable': True},
0803 {'name': 'updated_at', 'title': 'Updated', 'orderable': True},
0804 ]
0805 context = {
0806 'table_title': 'Production Tasks',
0807 'table_description': 'Production task compositions (Dataset + Config).',
0808 'ajax_url': reverse('pcs:prod_tasks_datatable_ajax'),
0809 'columns': columns,
0810 }
0811 return render(request, 'pcs/prod_tasks_list.html', context)
0812
0813
0814 def prod_tasks_datatable_ajax(request):
0815 col_names = ['name', 'status', 'dataset__dataset_name', 'prod_config__name',
0816 'created_by', 'updated_at']
0817 dt = DataTablesProcessor(request, col_names, default_order_column=5, default_order_direction='desc')
0818
0819 qs = ProdTask.objects.select_related('dataset', 'prod_config')
0820 records_total = qs.count()
0821 search_fields = ['name', 'description', 'dataset__dataset_name', 'prod_config__name', 'created_by']
0822 qs = dt.apply_search(qs, search_fields)
0823 records_filtered = qs.count()
0824 qs = qs.order_by(dt.get_order_by())
0825 page = dt.apply_pagination(qs)
0826
0827 status_colors = {'draft': 'secondary', 'ready': 'primary', 'submitted': 'info',
0828 'completed': 'success', 'failed': 'danger'}
0829 data = []
0830 for t in page:
0831 detail_url = reverse('pcs:prod_task_detail', args=[t.pk])
0832 color = status_colors.get(t.status, 'secondary')
0833 data.append([
0834 f'<a href="{detail_url}">{t.name}</a>',
0835 f'<span class="badge bg-{color}">{t.status}</span>',
0836 t.dataset.dataset_name,
0837 t.prod_config.name,
0838 t.created_by,
0839 format_datetime(t.updated_at),
0840 ])
0841
0842 return dt.create_response(data, records_total, records_filtered)
0843
0844
0845 def prod_task_detail(request, pk):
0846 from .commands import build_task_params
0847 task = get_object_or_404(
0848 ProdTask.objects.select_related(
0849 'dataset', 'dataset__physics_tag', 'dataset__evgen_tag',
0850 'dataset__simu_tag', 'dataset__reco_tag', 'prod_config',
0851 ),
0852 pk=pk,
0853 )
0854 try:
0855 task_params = build_task_params(task)
0856 task_params_json = json.dumps(task_params, indent=2, sort_keys=False, default=str)
0857 task_params_error = None
0858 except Exception as e:
0859 task_params_json = None
0860 task_params_error = str(e)
0861 return render(request, 'pcs/prod_task_detail.html', {
0862 'task': task,
0863 'task_params_json': task_params_json,
0864 'task_params_error': task_params_error,
0865 })
0866
0867
0868 def prod_task_compose(request):
0869 """Two-pane compose UI for building production tasks."""
0870 from .commands import build_task_params
0871
0872 datasets_qs = Dataset.objects.filter(block_num=1).select_related(
0873 'physics_tag', 'evgen_tag', 'simu_tag', 'reco_tag',
0874 ).order_by('-created_at')
0875 datasets_data = []
0876 for ds in datasets_qs:
0877 datasets_data.append({
0878 'id': ds.id,
0879 'dataset_name': ds.dataset_name,
0880 'did': ds.did,
0881 'scope': ds.scope,
0882 'detector_version': ds.detector_version,
0883 'detector_config': ds.detector_config,
0884 'physics_tag': {'label': ds.physics_tag.tag_label, 'description': ds.physics_tag.description,
0885 'parameters': ds.physics_tag.parameters},
0886 'evgen_tag': {'label': ds.evgen_tag.tag_label, 'description': ds.evgen_tag.description,
0887 'parameters': ds.evgen_tag.parameters},
0888 'simu_tag': {'label': ds.simu_tag.tag_label, 'description': ds.simu_tag.description,
0889 'parameters': ds.simu_tag.parameters},
0890 'reco_tag': {'label': ds.reco_tag.tag_label, 'description': ds.reco_tag.description,
0891 'parameters': ds.reco_tag.parameters},
0892 'created_by': ds.created_by,
0893 'created_at': ds.created_at.strftime('%Y-%m-%d %H:%M'),
0894 })
0895
0896 configs_qs = ProdConfig.objects.order_by('-updated_at')
0897 configs_data = []
0898 for pc in configs_qs:
0899 configs_data.append({
0900 'id': pc.id,
0901 'name': pc.name,
0902 'description': pc.description,
0903 'jug_xl_tag': pc.jug_xl_tag,
0904 'container_image': pc.container_image,
0905 'bg_mixing': pc.bg_mixing,
0906 'bg_cross_section': pc.bg_cross_section,
0907 'bg_evtgen_file': pc.bg_evtgen_file,
0908 'copy_reco': pc.copy_reco,
0909 'copy_full': pc.copy_full,
0910 'copy_log': pc.copy_log,
0911 'use_rucio': pc.use_rucio,
0912 'target_hours_per_job': str(pc.target_hours_per_job) if pc.target_hours_per_job else '',
0913 'events_per_task': pc.events_per_task,
0914 'panda_site': pc.panda_site,
0915 'panda_queue': pc.panda_queue,
0916 'panda_working_group': pc.panda_working_group,
0917 'panda_resource_type': pc.panda_resource_type,
0918 'rucio_rse': pc.rucio_rse,
0919 'data': pc.data or {},
0920 'created_by': pc.created_by,
0921 'updated_at': pc.updated_at.strftime('%Y-%m-%d %H:%M'),
0922 })
0923
0924 tasks_qs = ProdTask.objects.select_related(
0925 'dataset', 'dataset__physics_tag', 'dataset__evgen_tag',
0926 'dataset__simu_tag', 'dataset__reco_tag', 'prod_config',
0927 ).order_by('-updated_at')
0928 tasks_data = []
0929 for t in tasks_qs:
0930 try:
0931 task_params = build_task_params(t)
0932 task_params_json = json.dumps(task_params, indent=2, default=str)
0933 except Exception as e:
0934 task_params_json = f'// Error building taskParamMap: {e}'
0935 tasks_data.append({
0936 'id': t.id,
0937 'name': t.name,
0938 'status': t.status,
0939 'dataset_id': t.dataset_id,
0940 'dataset_name': t.dataset.dataset_name,
0941 'prod_config_id': t.prod_config_id,
0942 'prod_config_name': t.prod_config.name,
0943 'csv_file': t.csv_file,
0944 'overrides': t.overrides or {},
0945 'description': t.description,
0946 'condor_command': t.condor_command,
0947 'panda_command': t.panda_command,
0948 'task_params_json': task_params_json,
0949 'created_by': t.created_by,
0950 'updated_at': t.updated_at.strftime('%Y-%m-%d %H:%M'),
0951 })
0952
0953 context = {
0954 'datasets_json': json.dumps(datasets_data),
0955 'configs_json': json.dumps(configs_data),
0956 'tasks_json': json.dumps(tasks_data),
0957 'selected_item_json': json.dumps(request.GET.get('selected') or None),
0958 'username': request.user.username if request.user.is_authenticated else '',
0959 }
0960 return render(request, 'pcs/prod_task_compose.html', context)
0961
0962
0963 @login_required
0964 def prod_task_delete(request, pk):
0965 if request.method != 'POST':
0966 return redirect('pcs:prod_task_detail', pk=pk)
0967 task = get_object_or_404(ProdTask, pk=pk)
0968 if task.status != 'draft':
0969 messages.error(request, "Only draft tasks can be deleted.")
0970 return redirect('pcs:prod_task_detail', pk=pk)
0971 task.delete()
0972 messages.success(request, f"Task '{task.name}' deleted.")
0973 return redirect('pcs:prod_tasks_list')
0974
0975
0976 def prod_task_generate_commands(request, pk):
0977 """JSON endpoint: regenerate and return commands for a ProdTask."""
0978 task = get_object_or_404(
0979 ProdTask.objects.select_related(
0980 'dataset', 'dataset__physics_tag', 'dataset__evgen_tag',
0981 'dataset__simu_tag', 'dataset__reco_tag', 'prod_config',
0982 ),
0983 pk=pk,
0984 )
0985 task.generate_commands()
0986 task.save(update_fields=['condor_command', 'panda_command', 'updated_at'])
0987 return JsonResponse({
0988 'condor_command': task.condor_command,
0989 'panda_command': task.panda_command,
0990 })