chore: merge main into feat/tzaware

This commit is contained in:
Jennifer Richards 2022-10-28 14:43:50 -03:00 committed by GitHub
commit c4b9cb20f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 164 additions and 51 deletions

View file

@ -4,6 +4,8 @@ n-theme
.app-error(v-if='siteStore.criticalError')
i.bi.bi-x-octagon-fill.me-2
span {{siteStore.criticalError}}
.app-error-link(v-if='siteStore.criticalError && siteStore.criticalErrorLink')
a(:href='siteStore.criticalErrorLink') {{siteStore.criticalErrorLinkText}} #[i.bi.bi-arrow-right-square-fill.ms-2]
.app-container(ref='appContainer')
router-view.meeting
</template>
@ -56,4 +58,23 @@ onBeforeUnmount(() => {
padding: 1rem;
text-align: center;
}
.app-error-link {
background-color: lighten($red-100, 5%);
border-radius: 0 0 5px 5px;
color: #FFF;
font-weight: 500;
font-size: .9em;
padding: .7rem 1rem;
text-align: center;
a {
color: $red-700;
text-decoration: none;
&:hover, &:focus {
text-decoration: underline;
}
}
}
</style>

View file

@ -152,6 +152,7 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { getUrl } from '../shared/urls'
// PROPS
@ -205,10 +206,22 @@ const eventDetails = computed(() => {
title: props.event.type === 'regular' ? `${props.event.groupName} (${props.event.acronym})` : props.event.name,
showAgenda: props.event.flags.showAgenda,
materialsUrl: materialsUrl,
detailsUrl: `/meeting/${agendaStore.meeting.number}/session/${props.event.acronym}/`,
tarUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.tgz`,
pdfUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.pdf`,
notepadUrl: `https://notes.ietf.org/notes-ietf-${agendaStore.meeting.number}-${props.event.type === 'plenary' ? 'plenary' : props.event.acronym}`,
detailsUrl: getUrl('meetingDetails', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: props.event.acronym
}),
tarUrl: getUrl('meetingMaterialsTar', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: props.event.acronym
}),
pdfUrl: getUrl('meetingMaterialsPdf', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: props.event.acronym
}),
notepadUrl: getUrl('meetingNotes', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: props.event.type === 'plenary' ? 'plenary' : props.event.acronym
})
}
})

View file

@ -60,7 +60,7 @@ n-drawer(v-model:show='state.isShown', placement='bottom', :height='state.drawer
)
template(#trigger)
span.badge BoF
span #[a(href='https://www.ietf.org/how/bofs/', target='_blank') Birds of a Feather] sessions (BoFs) are initial discussions about a particular topic of interest to the IETF community.
span #[a(:href='getUrl(`bofDefinition`)', target='_blank') Birds of a Feather] sessions (BoFs) are initial discussions about a particular topic of interest to the IETF community.
</template>
<script setup>
@ -77,6 +77,7 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { getUrl } from '../shared/urls'
// STORES

View file

@ -32,6 +32,7 @@ import {
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store';
import { getUrl } from '../shared/urls'
// MESSAGE PROVIDER
@ -64,11 +65,11 @@ function downloadIcs (key) {
let icsUrl = ''
if (agendaStore.pickerMode) {
const sessionKeywords = agendaStore.scheduleAdjusted.map(s => s.sessionKeyword)
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics?show=${sessionKeywords.join(',')}`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}?show=${sessionKeywords.join(',')}`
} else if (agendaStore.selectedCatSubs.length > 0) {
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics?show=${agendaStore.selectedCatSubs.join(',')}`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}?show=${agendaStore.selectedCatSubs.join(',')}`
} else {
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}`
}
if (key === 'subscribe') {
window.location.assign(`webcal://${window.location.host}${icsUrl}`)

View file

@ -120,7 +120,8 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store';
import { useSiteStore } from '../shared/store'
import { getUrl } from '../shared/urls'
// MESSAGE PROVIDER
@ -181,11 +182,11 @@ function downloadIcs (key) {
let icsUrl = ''
if (agendaStore.pickerMode) {
const sessionKeywords = agendaStore.scheduleAdjusted.map(s => s.sessionKeyword)
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics?show=${sessionKeywords.join(',')}`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}?show=${sessionKeywords.join(',')}`
} else if (agendaStore.selectedCatSubs.length > 0) {
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics?show=${agendaStore.selectedCatSubs.join(',')}`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}?show=${agendaStore.selectedCatSubs.join(',')}`
} else {
icsUrl = `/meeting/${agendaStore.meeting.number}/agenda.ics`
icsUrl = `${getUrl('meetingCalIcs', { meetingNumber: agendaStore.meeting.number })}`
}
if (key === 'subscribe') {
window.location.assign(`webcal://${window.location.host}${icsUrl}`)

View file

@ -202,6 +202,7 @@ import AgendaDetailsModal from './AgendaDetailsModal.vue'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
import { getUrl } from '../shared/urls'
// MESSAGE PROVIDER
@ -275,14 +276,20 @@ const meetingEvents = computed(() => {
id: `lnk-${item.id}-tar`,
label: 'Download meeting materials as .tar archive',
icon: 'file-zip',
href: `/meeting/${agendaStore.meeting.number}/agenda/${item.acronym}-drafts.tgz`,
href: getUrl('meetingMaterialsTar', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: item.acronym
}),
color: 'brown'
})
links.push({
id: `lnk-${item.id}-pdf`,
label: 'Download meeting materials as PDF file',
icon: 'file-pdf',
href: `/meeting/${agendaStore.meeting.number}/agenda/${item.acronym}-drafts.pdf`,
href: getUrl('meetingMaterialsPdf', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: item.acronym
}),
color: 'red'
})
}
@ -291,7 +298,10 @@ const meetingEvents = computed(() => {
id: `lnk-${item.id}-note`,
label: 'Notepad for note-takers',
icon: 'journal-text',
href: `https://notes.ietf.org/notes-ietf-${agendaStore.meeting.number}-${item.type === 'plenary' ? 'plenary' : item.acronym}`,
href: getUrl('meetingNotes', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: item.type === 'plenary' ? 'plenary' : item.acronym
}),
color: 'blue'
})
}
@ -404,7 +414,10 @@ const meetingEvents = computed(() => {
id: `lnk-${item.id}-rec`,
label: 'Session recording',
icon: 'film',
href: `https://www.meetecho.com/ietf${agendaStore.meeting.number}/recordings#${item.acronym.toUpperCase()}`,
href: getUrl('meetingMeetechoRecordings', {
meetingNumber: agendaStore.meeting.number,
eventAcronym: item.acronym.toUpperCase()
}),
color: 'purple'
})
}

View file

@ -183,7 +183,9 @@ export const useAgendaStore = defineStore('agenda', {
console.error(err)
const siteStore = useSiteStore()
siteStore.$patch({
criticalError: `Failed to load this meeting: ${err.message}`
criticalError: `Failed to load this meeting: ${err.message}`,
criticalErrorLink: meetingNumber ? `/meeting/${meetingNumber}/agenda.txt` : `/meeting/agenda.txt`,
criticalErrorLinkText: 'Switch to text-only agenda version'
})
}

View file

@ -3,6 +3,8 @@ import { defineStore } from 'pinia'
export const useSiteStore = defineStore('site', {
state: () => ({
criticalError: null,
criticalErrorLink: null,
criticalErrorLinkText: null,
isMobile: /Mobi/i.test(navigator.userAgent),
viewport: Math.round(window.innerWidth)
})

30
client/shared/urls.js Normal file
View file

@ -0,0 +1,30 @@
import template from 'lodash-es/template'
import transform from 'lodash-es/transform'
const urls = {
bofDefinition: 'https://www.ietf.org/how/bofs/',
meetingCalIcs: '/meeting/{meetingNumber}/agenda.ics',
meetingDetails: '/meeting/{meetingNumber}/session/{eventAcronym}/',
meetingMaterialsPdf: '/meeting/{meetingNumber}/agenda/{eventAcronym}-drafts.pdf',
meetingMaterialsTar: '/meeting/{meetingNumber}/agenda/{eventAcronym}-drafts.tgz',
meetingMeetechoRecordings: 'https://www.meetecho.com/ietf{meetingNumber}/recordings#{eventAcronym}',
meetingNotes: 'https://notes.ietf.org/notes-ietf-{meetingNumber}-{eventAcronym}'
}
const interpolate = /{([\s\S]+?)}/g
const compiled = transform(urls, (result, value, key) => {
result[key] = template(value, { interpolate })
}, {})
/**
* Get an URL and replace tokens with provided values.
*
* @param {string} key The key of the URL template to use.
* @param {Object} [tokens] An object of tokens to replace in the URL template.
* @returns {string} URL with tokens replaced with the provided values.
*/
export const getUrl = (key, tokens = {}) => {
if (!key) { throw new Error('Must provide a key for getUrl()') }
if (!compiled[key]) { throw new Error('Invalid getUrl() key') }
return compiled[key](tokens)
}

View file

@ -6,7 +6,7 @@
{% if template_list %}
<ul>
{% for template in template_list %}
<li><a href="{% url "ietf.dbtemplate.views.template_edit" group.acronym template.id %}">{{ template }}</a></li>
<li><a href="{% url "ietf.dbtemplate.views.group_template_edit" group.acronym template.id %}">{{ template }}</a></li>
{% endfor %}
</ul>
{% else %}

View file

@ -34,7 +34,7 @@
<h2>Template content</h2>
<div class="card">
<p class="pasted">{{ template.content }}</p>
<pre class="pasted">{{ template.content|escape }}</pre>
</div>
{% endblock %}

View file

@ -58,20 +58,20 @@ from ietf.meeting.models import Meeting, Room, Constraint, Session, ResourceAsso
class Command(BaseCommand):
help = "Create (or delete) a dummy meeting for test and development purposes."
help = "Create (or delete) a meeting for test and development purposes."
def add_arguments(self, parser):
parser.add_argument('--delete', dest='delete', action='store_true', help='Delete the test and development dummy meeting')
parser.add_argument('--delete', dest='delete', action='store_true', help='Delete the test and development meeting')
parser.add_argument('--old-conflicts', dest='old_conflicts', action='store_true',
help='Use old conflict types ("conflict", "conflic2", "conflic3") instead of new ("chair_conflict", "tech_overlap", "key_participant")')
parser.add_argument(
'--start-date',
help='Start date for the dummy meeting (yyyy-mm-dd, defaults to 2019-11-16)',
help='Start date for the test meeting (yyyy-mm-dd, defaults to 2019-11-16)',
type=lambda s: datetime.datetime.strptime(s, '%Y-%m-%d').date(),
default='2019-11-16',
)
parser.add_argument('--tz', default='UTC',
help='Time zone for created meeting. Defaults to UTC. Use "" to disable.')
help='Time zone for test meeting. Defaults to UTC. Use "" to disable.')
def _meeting_datetime(self, day, *time_args):
"""Generate a datetime on a meeting day"""
@ -84,7 +84,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]:
raise EnvironmentError("Refusing to create a dummy meetng on a production server")
raise EnvironmentError("Refusing to create a test meetng on a production server")
opt_delete = options.get('delete', False)
opt_use_old_conflicts = options.get('old_conflicts', False)
@ -93,9 +93,9 @@ class Command(BaseCommand):
if opt_delete:
if Meeting.objects.filter(number='999').exists():
Meeting.objects.filter(number='999').delete()
self.stdout.write("Deleted dummy meeting IETF 999 and its related objects.")
self.stdout.write("Deleted test meeting IETF 999 and its related objects.")
else:
self.stderr.write("Dummy meeting IETF 999 does not exist; nothing to do.\n")
self.stderr.write("Test meeting IETF 999 does not exist; nothing to do.\n")
else:
try:
self.meeting_tz = pytz.timezone(meeting_tzname)
@ -103,12 +103,12 @@ class Command(BaseCommand):
raise CommandError("{} is not a recognized time zone.".format(meeting_tzname))
if Meeting.objects.filter(number='999').exists():
self.stderr.write("Dummy meeting IETF 999 already exists; nothing to do.\n")
self.stderr.write("Test meeting IETF 999 already exists; nothing to do.\n")
else:
transaction.set_autocommit(False)
if self.start_date.isoweekday() != 6:
self.stderr.write("Warning: dummy meeting does not start on Saturday, watch out for bugs")
self.stderr.write("Warning: test meeting does not start on Saturday, watch out for bugs")
m = Meeting.objects.create(
number='999',

View file

@ -674,7 +674,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
# timeslot structure will be neighbors. The grouping algorithm relies on this!
room_data[room.pk]['start_and_duration'],
# Within each group, sort higher capacity rooms first.
room.capacity,
-room.capacity if room.capacity is not None else 1, # sort rooms with capacity = None at end
# Finally, sort alphabetically by name
room.name
)
@ -3486,8 +3486,12 @@ def upcoming(request):
entries = list(ietf_meetings)
entries.extend(list(interim_sessions))
entries.sort(key = lambda o: pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o,Meeting) else o.official_timeslotassignment().timeslot.utc_start_time())
entries.sort(
key=lambda o: (
pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o, Meeting) else o.official_timeslotassignment().timeslot.utc_start_time(),
o.number if isinstance(o, Meeting) else o.meeting.number,
)
)
for o in entries:
if isinstance(o, Meeting):
o.start_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.date, datetime.time.min)).timestamp())
@ -3550,7 +3554,7 @@ def upcoming_ical(request):
session__in=[s.pk for m in meetings for s in m.sessions if m.type_id != 'ietf'],
timeslot__time__gte=today,
).order_by(
'schedule__meeting__date', 'session__type', 'timeslot__time'
'schedule__meeting__date', 'session__type', 'timeslot__time', 'schedule__meeting__number',
).select_related(
'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting'
).distinct())

View file

@ -14,47 +14,47 @@ from ietf.group.models import Group
from ietf.person.models import User
class Command(BaseCommand):
help = ("Create (or delete) a dummy nomcom for test and development purposes.")
help = ("Create (or delete) a nomcom for test and development purposes.")
def add_arguments(self, parser):
parser.add_argument('--delete', dest='delete', action='store_true', help='Delete the test and development dummy nomcom')
parser.add_argument('--delete', dest='delete', action='store_true', help='Delete the test and development nomcom')
def handle(self, *args, **options):
if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]:
raise EnvironmentError("Refusing to create a dummy nomcom on a production server")
raise EnvironmentError("Refusing to create a test nomcom on a production server")
opt_delete = options.get('delete', False)
if opt_delete:
if Group.objects.filter(acronym='nomcom7437').exists():
Group.objects.filter(acronym='nomcom7437').delete()
User.objects.filter(username__in=['dummychair','dummymember','dummycandidate']).delete()
self.stdout.write("Deleted dummy group 'nomcom7437' and its related objects.")
User.objects.filter(username__in=['testchair','testmember','testcandidate']).delete()
self.stdout.write("Deleted test group 'nomcom7437' and its related objects.")
else:
self.stderr.write("Dummy nomcom 'nomcom7437' does not exist; nothing to do.\n")
self.stderr.write("test nomcom 'nomcom7437' does not exist; nothing to do.\n")
else:
if Group.objects.filter(acronym='nomcom7437').exists():
self.stderr.write("Dummy nomcom 'nomcom7437' already exists; nothing to do.\n")
self.stderr.write("test nomcom 'nomcom7437' already exists; nothing to do.\n")
else:
nc = NomComFactory.create(**nomcom_kwargs_for_year(year=7437,
populate_personnel=False,
populate_positions=False))
e = EmailFactory(person__name='Dummy Chair', address='dummychair@example.com', person__user__username='dummychair', person__default_emails=False, origin='dummychair')
e = EmailFactory(person__name='Test Chair', address='testchair@example.com', person__user__username='testchair', person__default_emails=False, origin='testchair')
e.person.user.set_password('password')
e.person.user.save()
nc.group.role_set.create(name_id='chair',person=e.person,email=e)
e = EmailFactory(person__name='Dummy Member', address='dummymember@example.com', person__user__username='dummymember', person__default_emails=False, origin='dummymember')
e = EmailFactory(person__name='Test Member', address='testmember@example.com', person__user__username='testmember', person__default_emails=False, origin='testmember')
e.person.user.set_password('password')
e.person.user.save()
nc.group.role_set.create(name_id='member',person=e.person,email=e)
e = EmailFactory(person__name='Dummy Candidate', address='dummycandidate@example.com', person__user__username='dummycandidate', person__default_emails=False, origin='dummycandidate')
e = EmailFactory(person__name='Test Candidate', address='testcandidate@example.com', person__user__username='testcandidate', person__default_emails=False, origin='testcandidate')
e.person.user.set_password('password')
e.person.user.save()
NomineePositionFactory(nominee__nomcom=nc, nominee__person=e.person,
position__nomcom=nc, position__name='Dummy Area Director', position__is_iesg_position=True,
position__nomcom=nc, position__name='Test Area Director', position__is_iesg_position=True,
)
self.stdout.write("%s\n" % key)

View file

@ -64,9 +64,34 @@ body {
color: #999;
z-index: 2000000000;
}
#app-loading-footer {
position: absolute;
text-align: center;
bottom: 0;
left: 0;
right: 0;
z-index: 1000000000;
}
#app-loading-footer > a {
text-decoration: none;
font-weight: 500;
font-size: .9em;
color: #0aa2c0;
display: inline-block;
padding: 8px 16px;
background-color: #F9F9F9;
border-radius: 5px;
margin-bottom: 12px;
}
{% endblock %}
{% block content %}
{% origin %}
<div id="app"></div>
<div id="app-loading"></div>
<div id="app-loading">
<div id="app-loading-footer">
<a href="/meeting/{{ meetingData.meetingNumber }}/agenda.txt">Switch to text-only version &#11166;</a>
</div>
</div>
{% endblock %}

View file

@ -78,9 +78,9 @@
<dt class="col-sm-2">
Feedback
</dt>
<dd class="col-sm-10 pasted">
<dd class="col-sm-10 pasted"><pre>
{% decrypt feedback.comments request year 1 %}
</dd>
</pre></dd>
</dl>
{% if not forloop.last %}<hr>{% endif %}
{% endif %}

View file

@ -61,7 +61,7 @@
{% endfor %}
<button class="btn btn-primary"
type="submit"
name="end">Save feedback</button>
name="end" value="1">Save feedback</button>
<a class="btn btn-secondary float-end"
href="{% url 'ietf.nomcom.views.view_feedback_pending' year %}">Back</a>
{% else %}

View file

@ -41,9 +41,9 @@
<dt class="col-sm-2">
Feedback
</dt>
<dd class="col-sm-10 pasted">
<dd class="col-sm-10 pasted"><pre>
{% decrypt feedback.comments request year 1 %}
</dd>
</pre></dd>
</dl>
{% if not forloop.last %}<hr>{% endif %}
{% endif %}

View file

@ -42,9 +42,9 @@
<dt class="col-sm-2">
Feedback
</dt>
<dd class="col-sm-10 pasted">
<dd class="col-sm-10 pasted"><pre>
{% decrypt feedback.comments request year 1 %}
</dd>
</pre></dd>
</dl>
{% endfor %}
</div>