Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:25

0001 """AI app pages: the AI proposal list."""
0002 from django.db.models import Count
0003 from django.shortcuts import render
0004 
0005 from .models import Proposal
0006 
0007 
0008 def ai_proposals(request):
0009     """The AI proposal list (AI_PROPOSALS.md).
0010 
0011     Pending proposals for review, decision history, and per-proposer track
0012     records. Read-open — visible-but-inert; decisions require sign-in and
0013     act through the same proposal-decide service as the catalog and
0014     compose surfaces.
0015     """
0016     def url_with(**updates):
0017         params = request.GET.copy()
0018         for key, value in updates.items():
0019             if value:
0020                 params[key] = value
0021             else:
0022                 params.pop(key, None)
0023         encoded = params.urlencode()
0024         return f'{request.path}?{encoded}' if encoded else request.path
0025 
0026     filters = {key: (request.GET.get(key) or '').strip()
0027                for key in ('status', 'action', 'change', 'decision',
0028                            'quality', 'proposer', 'batch', 'subject')}
0029     status_filter = filters['status'] or 'all'
0030 
0031     qs = Proposal.objects.all()
0032     if status_filter != 'all':
0033         qs = qs.filter(status=status_filter)
0034     if filters['subject']:
0035         qs = qs.filter(subject_key=filters['subject'])
0036     if filters['action']:
0037         qs = qs.filter(action=filters['action'])
0038     if filters['change'] and ':' in filters['change']:
0039         prev_state, _, new_state = filters['change'].partition(':')
0040         qs = qs.filter(precondition__prev_state=prev_state,
0041                        payload__state=new_state)
0042     if filters['decision']:
0043         qs = qs.filter(decided_by=filters['decision'])
0044     if filters['quality']:
0045         qs = qs.filter(quality=filters['quality'])
0046     if filters['proposer']:
0047         qs = qs.filter(proposer=filters['proposer'])
0048     if filters['batch']:
0049         qs = qs.filter(batch_id=filters['batch'])
0050 
0051     total_count = qs.count()
0052     rows = list(qs.order_by('-created_at')[:500])
0053 
0054     # Facet rows: per-dimension global counts, links preserving the other
0055     # active filters; every dimension leads with All (the default).
0056     everything = Proposal.objects.all()
0057 
0058     def facet_row(title, param, pairs, label_of=str):
0059         items = [{
0060             'label': label_of(value), 'count': count,
0061             'url': url_with(**{param: value}),
0062             'active': filters[param] == value,
0063         } for value, count in pairs if value]
0064         return {'title': title, 'items': items,
0065                 'all_url': url_with(**{param: ''}),
0066                 'all_active': not filters[param]}
0067 
0068     status_counts = dict(everything.values_list('status').annotate(Count('id')))
0069     status_order = ['proposed', 'executed', 'denied', 'withdrawn',
0070                     'stale', 'approved_pending_execution']
0071     status_row = {
0072         'title': 'Status',
0073         'items': [{'label': s, 'count': status_counts.get(s, 0),
0074                    'url': url_with(status=s), 'active': status_filter == s}
0075                   for s in status_order
0076                   if status_counts.get(s, 0) or s == 'proposed'],
0077         'all_url': url_with(status=''),
0078         'all_active': status_filter == 'all',
0079     }
0080 
0081     action_labels = {'propagation': 'campaign propagation'}
0082     facet_rows = [
0083         status_row,
0084         facet_row('Action', 'action',
0085                   everything.values_list('action').annotate(Count('id'))
0086                   .order_by('action'),
0087                   lambda a: action_labels.get(a, a)),
0088         facet_row('Change', 'change', [
0089             (f'{prev}:{new}', count)
0090             for prev, new, count in everything
0091             .values_list('precondition__prev_state', 'payload__state')
0092             .annotate(Count('id')).order_by()
0093             if prev and new],
0094             lambda v: v.replace(':', ' → ')),
0095         facet_row('Decision', 'decision',
0096                   everything.exclude(decided_by='')
0097                   .values_list('decided_by').annotate(Count('id'))
0098                   .order_by('decided_by')),
0099         facet_row('Quality', 'quality',
0100                   everything.exclude(quality='')
0101                   .values_list('quality').annotate(Count('id'))
0102                   .order_by('quality')),
0103         facet_row('Proposer', 'proposer',
0104                   everything.exclude(proposer='')
0105                   .values_list('proposer').annotate(Count('id'))
0106                   .order_by('proposer')),
0107         facet_row('Batch', 'batch',
0108                   everything.exclude(batch_id='')
0109                   .values_list('batch_id').annotate(Count('id'))
0110                   .order_by('batch_id')),
0111     ]
0112 
0113     proposer_stats = []
0114     for proposer in (Proposal.objects.exclude(proposer='').order_by()
0115                      .values_list('proposer', flat=True).distinct()):
0116         base = Proposal.objects.filter(proposer=proposer)
0117         proposer_stats.append({
0118             'proposer': proposer,
0119             'total': base.count(),
0120             'pending': base.filter(status='proposed').count(),
0121             'executed': base.filter(status='executed').count(),
0122             'denied': base.filter(status='denied').count(),
0123             'wrong': base.filter(quality='wrong').count(),
0124         })
0125 
0126     return render(request, 'ai/proposals.html', {
0127         'rows': rows,
0128         'total_count': total_count,
0129         'shown_count': len(rows),
0130         'facet_rows': facet_rows,
0131         'subject_filter': filters['subject'],
0132         'subject_clear_url': url_with(subject=''),
0133         'proposer_stats': proposer_stats,
0134     })
0135 
0136 
0137 def _narrative_entry_from_page(page, client, *, with_versions=False,
0138                                 with_comments=True):
0139     """Shared shaping of a corun narrative page for templates."""
0140     from .assessments import render_assessment_markdown
0141     from .corun_client import CorunAPIError
0142 
0143     def _strip_h1(text):
0144         # The page header carries the title; drop the document's own
0145         # leading H1 from the rendered body to avoid repeating it. The
0146         # stored document keeps its H1 (it stands alone).
0147         stripped = text.lstrip()
0148         if stripped.startswith('# '):
0149             return stripped.split('\n', 1)[1] if '\n' in stripped else ''
0150         return text
0151 
0152     data = page.get('data') or {}
0153     content = page.get('content') or ''
0154     group_id = page.get('group_id') or page.get('id')
0155     entry = {
0156         'name': data.get('name', ''),
0157         'title': page.get('title') or data.get('name', ''),
0158         'version': page.get('version'),
0159         'updated': (page.get('modified_at') or page.get('created_at') or ''),
0160         'content': content,
0161         'html': render_assessment_markdown(_strip_h1(content)),
0162         'group_id': group_id,
0163         'versions': [],
0164         'comments': [],
0165     }
0166     if with_versions:
0167         try:
0168             for v in sorted(client.list_versions(group_id) or [],
0169                             key=lambda x: x.get('version', 0), reverse=True):
0170                 v_content = v.get('content') or ''
0171                 entry['versions'].append({
0172                     'version': v.get('version'),
0173                     'date': (v.get('created_at') or '')[:16].replace('T', ' '),
0174                     'author': (v.get('data') or {}).get('author', ''),
0175                     'lines': len(v_content.splitlines()),
0176                     'is_current': bool(v.get('is_current')),
0177                     'html': render_assessment_markdown(_strip_h1(v_content)),
0178                 })
0179         except CorunAPIError:
0180             entry['versions'] = []
0181     if with_comments:
0182         try:
0183             for c in client.list_comments(group_id) or []:
0184                 # The signed-in swf-monitor user is stamped in data.author
0185                 # at post time; corun's own author field is the API token's
0186                 # account, not the human.
0187                 stamped = (c.get('data') or {}).get('author', '')
0188                 fallback = c.get('author')
0189                 if isinstance(fallback, dict):
0190                     fallback = fallback.get('username', '')
0191                 entry['comments'].append({
0192                     'author': stamped or fallback or '',
0193                     'date': (c.get('created_at') or '')[:16].replace('T', ' '),
0194                     'content': c.get('content') or '',
0195                 })
0196         except CorunAPIError:
0197             entry['comments'] = []
0198     return entry
0199 
0200 
0201 def _narrative_pages(client):
0202     payload = client.list_pages(
0203         section='epicprod.narrative',
0204         artifact_type='campaign_narrative',
0205         limit=100,
0206     )
0207     if isinstance(payload, dict):
0208         return payload.get('results') or payload.get('items') or []
0209     return payload or []
0210 
0211 
0212 def ai_narratives(request):
0213     """The Campaign Narratives list (EPICPROD_NARRATIVES.md).
0214 
0215     Collapsible read view with comments; document management (editing,
0216     version history) lives on the per-document detail page. Read-open; a
0217     corun-ai failure renders as an error message, never an empty page.
0218     """
0219     from .corun_client import CorunAPIError, CorunClient, corun_configured
0220 
0221     entries, error = [], ''
0222     if not corun_configured():
0223         error = 'corun-ai is not configured on this deployment.'
0224     else:
0225         try:
0226             client = CorunClient()
0227             entries = [
0228                 _narrative_entry_from_page(p, client, with_comments=True)
0229                 for p in _narrative_pages(client)
0230             ]
0231         except CorunAPIError as exc:
0232             error = f'corun-ai retrieval failed: {exc}'
0233     # General items (standing context) precede campaign-specific ones;
0234     # newest first within each block. The series identity is data.name.
0235     general_entries = sorted(
0236         (e for e in entries if e['name'].startswith('campaign_general')),
0237         key=lambda e: e['updated'], reverse=True)
0238     campaign_entries = sorted(
0239         (e for e in entries if not e['name'].startswith('campaign_general')),
0240         key=lambda e: e['updated'], reverse=True)
0241     return render(request, 'ai/narratives.html',
0242                   {'general_entries': general_entries,
0243                    'campaign_entries': campaign_entries,
0244                    'entries': entries, 'error': error})
0245 
0246 
0247 def ai_narrative_detail(request, name):
0248     """One narrative document: content, expert editing, the version
0249     history (tjai-style, at the bottom), and comments."""
0250     from django.http import Http404
0251 
0252     from .corun_client import CorunAPIError, CorunClient, corun_configured
0253 
0254     if not corun_configured():
0255         raise Http404('corun-ai is not configured')
0256     try:
0257         client = CorunClient()
0258         page = next((p for p in _narrative_pages(client)
0259                      if (p.get('data') or {}).get('name') == name), None)
0260     except CorunAPIError as exc:
0261         return render(request, 'ai/narrative_detail.html',
0262                       {'entry': None, 'error': f'corun-ai retrieval failed: {exc}'})
0263     if page is None:
0264         raise Http404(f'No narrative named {name!r}')
0265     entry = _narrative_entry_from_page(page, client, with_versions=True,
0266                                        with_comments=True)
0267     return render(request, 'ai/narrative_detail.html',
0268                   {'entry': entry, 'error': ''})