Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/agenda/Agenda.vue
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ function reconnectScrollObservers () {
scrollObserver.disconnect()
visibleDays.length = 0
for (const mDay of agendaStore.meetingDays) {
const el = document.getElementById(`agenda-day-${mDay.slug}`)
const el = document.getElementById(mDay.slug)
el.dataset.dayId = mDay.slug.toString()
el.dataset.dayTs = mDay.ts
scrollObserver.observe(el)
Expand Down
54 changes: 47 additions & 7 deletions client/agenda/AgendaMobileBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

<script setup>
import { computed, h } from 'vue'

import {
NBadge,
NDropdown,
Expand All @@ -51,21 +50,48 @@ const siteStore = useSiteStore()

// Meeting Days

function optionToLink(opts){
const { key, label, icon } = opts

return {
...opts,
type: 'render',
render: () => h(
'a',
{
class: 'dropdown-link',
'data-testid': 'mobile-link',
href: `#${key}`
},
[
h(
'span',
icon()
),
h(
'span',
label
)
]
)
}
}

const jumpToDayOptions = computed(() => {
const days = []
if (agendaStore.isMeetingLive) {
days.push({
days.push(optionToLink({
label: 'Jump to Now',
key: 'now',
icon: () => h('i', { class: 'bi bi-arrow-down-right-square text-red' })
})
}))
}
for (const day of agendaStore.meetingDays) {
days.push({
days.push(optionToLink({
label: `Jump to ${day.label}`,
key: day.slug,
icon: () => h('i', { class: 'bi bi-arrow-down-right-square' })
})
}))
}
return days
})
Expand All @@ -90,14 +116,13 @@ const downloadIcsOptions = [
function jumpToDay (dayId) {
if (dayId === 'now') {
const lastEventId = agendaStore.findCurrentEventId()

if (lastEventId) {
document.getElementById(`agenda-rowid-${lastEventId}`)?.scrollIntoView(true)
} else {
message.warning('There is no event happening right now.')
}
} else {
document.getElementById(`agenda-day-${dayId}`)?.scrollIntoView(true)
document.getElementById(dayId)?.scrollIntoView(true)
}
}

Expand Down Expand Up @@ -162,4 +187,19 @@ function downloadIcs (key) {
}
}
}

.dropdown-link {
display: flex;
text-decoration:none;
gap: 0.2rem 0.5rem;
padding: 0.5em;
color: var(--bs-body-color);

&:hover,
&:focus {
background-color: var(--bs-dark-bg-subtle);
text-decoration: underline;
}
}

</style>
10 changes: 3 additions & 7 deletions client/agenda/AgendaQuickAccess.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
li.nav-item(v-for='day of agendaStore.meetingDays')
a.nav-link(
:class='agendaStore.dayIntersectId === day.slug ? `active` : ``'
:href='`#slot-` + day.slug'
:href='`#${day.slug}`'
@click='scrollToDay(day.slug, $event)'
)
i.bi.bi-arrow-right-short.d-none.d-xxl-inline.me-2
Expand All @@ -109,7 +109,6 @@
<script setup>
import { computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { DateTime } from 'luxon'
import {
NAffix,
NBadge,
Expand Down Expand Up @@ -200,14 +199,11 @@ function pickerDiscard () {
}
}

function scrollToDay (dayId, ev) {
ev.preventDefault()
document.getElementById(`agenda-day-${dayId}`)?.scrollIntoView(true)
function scrollToDay (daySlug, ev) {
document.getElementById(daySlug)?.scrollIntoView(true)
}

function scrollToNow (ev) {
ev.preventDefault()

const lastEventId = agendaStore.findCurrentEventId()

if (lastEventId) {
Expand Down
29 changes: 27 additions & 2 deletions client/agenda/AgendaScheduleList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
//- ROW - DAY HEADING -----------------------
template(v-if='item.displayType === `day`')
td(:id='`agenda-day-` + item.id', :colspan='pickerModeActive ? 6 : 5') {{item.date}}
td(:id='item.slug', :colspan='pickerModeActive ? 6 : 5') {{item.date}}
//- ROW - SESSION HEADING -------------------
template(v-else-if='item.displayType === `session-head`')
td.agenda-table-cell-check(v-if='pickerModeActive') &nbsp;
Expand Down Expand Up @@ -200,7 +200,7 @@ import {

import AgendaDetailsModal from './AgendaDetailsModal.vue'

import { useAgendaStore } from './store'
import { useAgendaStore, daySlugPrefix, daySlug } from './store'
import { useSiteStore } from '../shared/store'
import { getUrl } from '../shared/urls'

Expand Down Expand Up @@ -248,6 +248,7 @@ const meetingEvents = computed(() => {
if (itemDate.toISODate() !== acc.lastDate) {
acc.result.push({
id: item.id,
slug: daySlug(item),
key: `day-${itemDate.toISODate()}`,
displayType: 'day',
date: itemDate.toLocaleString(DateTime.DATE_HUGE),
Expand Down Expand Up @@ -575,6 +576,30 @@ function recalculateRedLine () {
}
}

/**
* On page load when browser location hash contains '#now' or '#agenda-day-*' then scroll accordingly
*/
;(function scrollToHashInit() {
if (!window.location.hash) {
return
}
if (!(window.location.hash === "#now" || window.location.hash.startsWith(`#${daySlugPrefix}`))) {
return
}
const unsubscribe = agendaStore.$subscribe((_mutation, agendaStoreState) => {
if (agendaStoreState.schedule.length === 0) {
return
}
unsubscribe() // we only need to scroll once, so unsubscribe from future updates
if(window.location.hash === "#now") {
const lastEventId = agendaStore.findCurrentEventId()
document.getElementById(`agenda-rowid-${lastEventId}`)?.scrollIntoView(true)
} else if(window.location.hash.startsWith(`#${daySlugPrefix}`)) {
document.getElementById(window.location.hash.substring(1))?.scrollIntoView(true)
}
})
})()

// MOUNTED

onMounted(() => {
Expand Down
7 changes: 6 additions & 1 deletion client/agenda/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const useAgendaStore = defineStore('agenda', {
meetingDays () {
const siteStore = useSiteStore()
return uniqBy(this.scheduleAdjusted, 'adjustedStartDate').sort().map(s => ({
slug: s.id.toString(),
slug: daySlug(s),
ts: s.adjustedStartDate,
label: siteStore.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
}))
Expand Down Expand Up @@ -292,3 +292,8 @@ function findFirstConferenceUrl (txt) {
} catch (err) { }
return null
}

export const daySlugPrefix = 'agenda-day-'
export function daySlug(s) {
return `${daySlugPrefix}${s.adjustedStartDate}` // eg 'agenda-day-2024-08-13'
}
4 changes: 2 additions & 2 deletions client/embedded.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import Embedded from './Embedded.vue'
import { createPiniaSingleton } from './shared/create-pinia-singleton'

// Initialize store (Pinia)

const pinia = createPinia()
const pinia = createPiniaSingleton()
pinia.use(piniaPersist)

// Mount App
Expand Down
4 changes: 2 additions & 2 deletions client/main.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import App from './App.vue'
import router from './router'
import { createPiniaSingleton } from './shared/create-pinia-singleton'

const app = createApp(App, {})

// Initialize store (Pinia)

const pinia = createPinia()
const pinia = createPiniaSingleton()
pinia.use(piniaPersist)
app.use(pinia)

Expand Down
6 changes: 6 additions & 0 deletions client/shared/create-pinia-singleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createPinia } from 'pinia'

export function createPiniaSingleton(){
window.pinia = window.pinia ?? createPinia()
return window.pinia
}
30 changes: 30 additions & 0 deletions ietf/doc/migrations/0023_bofreqspamstate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright The IETF Trust 2024, All Rights Reserved

from django.db import migrations


def forward(apps, schema_editor):
State = apps.get_model("doc", "State")
State.objects.get_or_create(
type_id="bofreq",
slug="spam",
defaults={"name": "Spam", "desc": "The BOF request is spam", "order": 5},
)


def reverse(apps, schema_editor):
State = apps.get_model("doc", "State")
Document = apps.get_model("doc", "Document")
assert not Document.objects.filter(
states__type="bofreq", states__slug="spam"
).exists()
State.objects.filter(type_id="bofreq", slug="spam").delete()


class Migration(migrations.Migration):

dependencies = [
("doc", "0022_remove_dochistory_internal_comments_and_more"),
]

operations = [migrations.RunPython(forward, reverse)]
13 changes: 11 additions & 2 deletions ietf/doc/templatetags/ballot_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,14 @@ def sort_key(t):
positions = list(ballot.active_balloter_positions().items())
positions.sort(key=sort_key)

request = context.get("request")
ballot_edit_return_point_param = f"ballot_edit_return_point={request.path}"

right_click_string = ''
if has_role(user, "Area Director"):
right_click_string = 'oncontextmenu="window.location.href=\'%s\';return false;"' % urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))
right_click_string = 'oncontextmenu="window.location.href=\'{}?{}\';return false;"'.format(
urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
ballot_edit_return_point_param)

my_blocking = False
for i, (balloter, pos) in enumerate(positions):
Expand All @@ -113,10 +118,14 @@ def sort_key(t):
typename = "RSAB"
else:
typename = "IESG"

modal_url = "{}?{}".format(
urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
ballot_edit_return_point_param)

res = ['<a %s href="%s" data-bs-toggle="modal" data-bs-target="#modal-%d" aria-label="%s positions" title="%s positions (click to show more)" class="ballot-icon"><table' % (
right_click_string,
urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
modal_url,
ballot.pk,
typename,
typename,)]
Expand Down
29 changes: 29 additions & 0 deletions ietf/doc/templatetags/document_type_badge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright The IETF Trust 2015-2020, All Rights Reserved
from django import template
from django.conf import settings
from django.template.loader import render_to_string
from ietf.utils.log import log

register = template.Library()


@register.simple_tag
def document_type_badge(doc, snapshot, submission, resurrected_by):
context = {"doc": doc, "snapshot": snapshot, "submission": submission, "resurrected_by": resurrected_by}
if doc.type_id == "rfc":
return render_to_string(
"doc/badge/doc-badge-rfc.html",
context,
)
elif doc.type_id == "draft":
return render_to_string(
"doc/badge/doc-badge-draft.html",
context,
)
else:
error_message = f"Unsupported document type {doc.type_id}."
if settings.SERVER_MODE != 'production':
raise ValueError(error_message)
else:
log(error_message)
return ""
23 changes: 11 additions & 12 deletions ietf/doc/tests_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ def test_cannot_edit_position_as_pre_ad(self):
r = self.client.post(url, dict(position="discuss", discuss="Test discuss text"))
self.assertEqual(r.status_code, 403)

# N.B. This test needs to be rewritten to exercise all types of ballots (iesg, irsg, rsab)
# and test against the output of the mailtriggers instead of looking for hardcoded values
# in the To and CC results. See #7864
def test_send_ballot_comment(self):
ad = Person.objects.get(user__username="ad")
draft = WgDraftFactory(ad=ad,group__acronym='mars')
Expand Down Expand Up @@ -1455,18 +1458,14 @@ def test_document_ballot_content_without_send_email_values(self):

class ReturnToUrlTests(TestCase):
def test_invalid_return_to_url(self):
self.assertRaises(
Exception,
lambda: parse_ballot_edit_return_point('/doc/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'),
)
self.assertRaises(
Exception,
lambda: parse_ballot_edit_return_point('/a-route-that-does-not-exist/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'),
)
self.assertRaises(
Exception,
lambda: parse_ballot_edit_return_point('https://example.com/phishing', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'),
)
with self.assertRaises(ValueError):
parse_ballot_edit_return_point('/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718')

with self.assertRaises(ValueError):
parse_ballot_edit_return_point('/a-route-that-does-not-exist/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718')

with self.assertRaises(ValueError):
parse_ballot_edit_return_point('https://example.com/phishing', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718')

def test_valid_default_return_to_url(self):
self.assertEqual(parse_ballot_edit_return_point(
Expand Down
Loading