Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-20 10:15:30

0001 {% extends "base.html" %}
0002 {% block title %}User admin{% endblock %}
0003 
0004 {% block content %}
0005 <div class="container-fluid px-3">
0006   <h2>User admin</h2>
0007 
0008   {% if forbidden %}
0009     <p>This page is for staff and accounts holding operations rights.</p>
0010   {% else %}
0011 
0012   <p>
0013     Reading the monitor needs a signed-in account and nothing more. Acting on
0014     the production system needs authority: membership of the eic GitHub
0015     organization, or rights granted here.
0016     {{ acting_count }} of {{ rows|length }} accounts may act.
0017   </p>
0018 
0019   <p>
0020     Membership is observed at sign-in and is shown here, not set here: a hand
0021     edit would be overwritten the next time the person signs in. Rights are
0022     set here, and a sign-in never touches them.
0023   </p>
0024 
0025   <ul>
0026     <li><strong>unset</strong> &mdash; the ordinary state: the account draws
0027       authority from its eic membership alone.</li>
0028     {% for level in rights_levels %}
0029       <li><strong>{{ level.value }}</strong> &mdash; {{ level.meaning }}</li>
0030     {% endfor %}
0031   </ul>
0032   <p>Roles are held beside the level, together or apart:</p>
0033   <ul>
0034     <li><strong>PAC</strong> &mdash; {{ pac_meaning }}
0035       {{ pac_count }} account{{ pac_count|pluralize }} hold it.</li>
0036     <li><strong>Ops</strong> &mdash; {{ ops_meaning }}
0037       {{ ops_count }} account{{ ops_count|pluralize }} hold it.</li>
0038   </ul>
0039 
0040   {% csrf_token %}
0041   <div id="user-admin-status" class="mb-2" style="min-height:1.5em;"></div>
0042 
0043   <table class="table table-striped table-bordered table-sm align-middle w-auto swf-sortable">
0044     <thead>
0045       <tr>
0046         <th>Account</th>
0047         <th>GitHub login</th>
0048         <th>Origin</th>
0049         <th>eic</th>
0050         <th title="When eic membership was last observed for the account: at sign-in through epic-devcloud.org, or by the backfill">Membership checked</th>
0051         <th>May act</th>
0052         <th>Rights</th>
0053         <th title="Physics analysis coordinator: may set request priorities">PAC</th>
0054         <th title="Operations: administers accounts, sets priorities, acts on the production system">Ops</th>
0055         <th></th>
0056       </tr>
0057     </thead>
0058     <tbody>
0059       {% for row in rows %}
0060       <tr data-username="{{ row.username }}">
0061         <td>{{ row.username }}{% if row.is_staff %} <span class="text-muted">(staff)</span>{% endif %}</td>
0062         <td>{% if row.github %}{{ row.github }}{% else %}<span class="text-muted">&mdash;</span>{% endif %}</td>
0063         <td>{{ row.origin|default:"&mdash;" }}</td>
0064         <td>
0065           {% if row.eic is True %}member
0066           {% elif row.eic is False %}not a member
0067           {% else %}<span class="text-muted">unset</span>{% endif %}
0068           {% if row.check_failed %}
0069             <div class="text-danger fw-semibold" title="{{ row.check_failed }}">last check failed
0070               <span data-relative-time="{{ row.check_failed_at }}"></span></div>
0071           {% endif %}
0072         </td>
0073         <td>
0074           {% if row.last_seen %}
0075             <span data-relative-time="{{ row.last_seen }}"></span>
0076           {% elif row.last_login %}
0077             <span data-relative-time="{{ row.last_login|date:'c' }}"></span>
0078           {% else %}
0079             <span class="text-muted">&mdash;</span>
0080           {% endif %}
0081         </td>
0082         <td class="js-may-act">{% if row.may_act %}yes{% else %}no{% endif %}</td>
0083         <td>
0084           <select class="form-select form-select-sm w-auto d-inline-block js-rights">
0085             <option value=""{% if not row.rights %} selected{% endif %}>unset</option>
0086             {% for value in rights_values %}
0087               <option value="{{ value }}"{% if row.rights == value %} selected{% endif %}>{{ value }}</option>
0088             {% endfor %}
0089           </select>
0090         </td>
0091         <td><input type="checkbox" class="form-check-input js-pac"{% if row.pac %} checked{% endif %} title="Physics analysis coordinator"></td>
0092         <td><input type="checkbox" class="form-check-input js-ops"{% if row.ops %} checked{% endif %} title="Operations"></td>
0093         {# Outline until the row is changed, then filled: Save per line, quiet. #}
0094         {# Row-size type in a visible colour, never smaller or greyer. #}
0095         <td><button type="button" class="btn btn-sm btn-outline-primary js-save">Save</button></td>
0096       </tr>
0097       {% endfor %}
0098     </tbody>
0099   </table>
0100   {% endif %}
0101 </div>
0102 
0103 <script>
0104 (function () {
0105   var url = "{% url 'monitor_app:user-rights' %}";
0106   var status = document.getElementById('user-admin-status');
0107   if (!status) { return; }
0108   // The internal face is a CILogon session with CSRF in force; the tunnel
0109   // exempts it. Send the token both ways (house pattern, pcs/find.html).
0110   var csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
0111   var csrf = csrfInput ? csrfInput.value : '';
0112 
0113   function say(text, ok) {
0114     status.textContent = text;
0115     status.style.color = ok ? '' : '#b02a37';
0116   }
0117 
0118   function setDirty(button, dirty) {
0119     button.classList.toggle('btn-primary', dirty);
0120     button.classList.toggle('btn-outline-primary', !dirty);
0121   }
0122 
0123   document.querySelectorAll('tr[data-username]').forEach(function (row) {
0124     var button = row.querySelector('.js-save');
0125     if (!button) { return; }
0126     var select = row.querySelector('.js-rights');
0127     var pac = row.querySelector('.js-pac');
0128     var ops = row.querySelector('.js-ops');
0129     var saved = select.value;
0130     var savedPac = pac.checked;
0131     var savedOps = ops.checked;
0132     function dirty() {
0133       return select.value !== saved || pac.checked !== savedPac || ops.checked !== savedOps;
0134     }
0135     select.addEventListener('change', function () { setDirty(button, dirty()); });
0136     pac.addEventListener('change', function () { setDirty(button, dirty()); });
0137     ops.addEventListener('change', function () { setDirty(button, dirty()); });
0138     button.addEventListener('click', function () {
0139       var username = row.getAttribute('data-username');
0140       var value = select.value;
0141       button.disabled = true;
0142       say('Saving ' + username + '...', true);
0143       fetch(url, {
0144         method: 'POST',
0145         headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrf},
0146         body: JSON.stringify({username: username, rights: value || null,
0147                               pac: pac.checked, ops: ops.checked})
0148       }).then(function (response) {
0149         return response.json().then(function (data) {
0150           return {ok: response.ok, data: data};
0151         });
0152       }).then(function (result) {
0153         button.disabled = false;
0154         if (!result.ok) {
0155           say(username + ': ' + (result.data.error || 'refused'), false);
0156           return;
0157         }
0158         row.querySelector('.js-may-act').textContent =
0159           result.data.may_act ? 'yes' : 'no';
0160         saved = result.data.authority.rights || '';
0161         savedPac = !!result.data.authority.pac;
0162         pac.checked = savedPac;
0163         savedOps = !!result.data.authority.ops;
0164         ops.checked = savedOps;
0165         setDirty(button, false);
0166         var rights = result.data.authority.rights;
0167         say(username + ': rights ' + (rights || 'unset') + ', may act ' +
0168             (result.data.may_act ? 'yes' : 'no') + ', PAC ' +
0169             (savedPac ? 'yes' : 'no') + ', Ops ' + (savedOps ? 'yes' : 'no'), true);
0170       }).catch(function (error) {
0171         button.disabled = false;
0172         say(username + ': ' + error, false);
0173       });
0174     });
0175   });
0176 })();
0177 </script>
0178 {% endblock %}