Skip to content

Commit

Permalink
ci: merge main to release (#7806)
Browse files Browse the repository at this point in the history
  • Loading branch information
rjsparks authored Aug 7, 2024
2 parents 7c89b4f + c4ff0a5 commit 162aa54
Show file tree
Hide file tree
Showing 67 changed files with 1,094 additions and 315 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Many developers are using [VS Code](https://code.visualstudio.com/) and taking a
If VS Code is not available to you, in your clone, type `cd docker; ./run`
Once the containers are started, run the tests to make sure your checkout is a good place to start from (all tests should pass - if any fail, ask for help at tools-develop@). Inside the app container's shell type:
Once the containers are started, run the tests to make sure your checkout is a good place to start from (all tests should pass - if any fail, ask for help at tools-help@). Inside the app container's shell type:
```sh
ietf/manage.py test --settings=settings_test
```
Expand Down
8 changes: 5 additions & 3 deletions client/Embedded.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<template lang="pug">
n-theme
n-message-provider
component(:is='currentComponent', :component-id='props.componentId')
n-notification-provider
n-message-provider
component(:is='currentComponent', :component-id='props.componentId')
</template>

<script setup>
import { defineAsyncComponent, markRaw, onMounted, ref } from 'vue'
import { NMessageProvider } from 'naive-ui'
import { NMessageProvider, NNotificationProvider } from 'naive-ui'
import NTheme from './components/n-theme.vue'
Expand All @@ -15,6 +16,7 @@ import NTheme from './components/n-theme.vue'
const availableComponents = {
ChatLog: defineAsyncComponent(() => import('./components/ChatLog.vue')),
Polls: defineAsyncComponent(() => import('./components/Polls.vue')),
Status: defineAsyncComponent(() => import('./components/Status.vue'))
}
// PROPS
Expand Down
2 changes: 1 addition & 1 deletion client/agenda/Agenda.vue
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ const meetingUpdated = computed(() => {
if (!agendaStore.meeting.updated) { return false }
const updatedDatetime = DateTime.fromISO(agendaStore.meeting.updated).setZone(agendaStore.timezone)
if (!updatedDatetime.isValid || updatedDatetime < DateTime.fromISO('1980-01-01')) {
if (!updatedDatetime.isValid) {
return false
}
Expand Down
2 changes: 1 addition & 1 deletion client/agenda/AgendaScheduleList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ const meetingEvents = computed(() => {
color: 'red'
})
}
if (agendaStore.useNotes) {
if (agendaStore.usesNotes) {
links.push({
id: `lnk-${item.id}-note`,
label: 'Notepad for note-takers',
Expand Down
4 changes: 2 additions & 2 deletions client/agenda/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const useAgendaStore = defineStore('agenda', {
selectedCatSubs: [],
settingsShown: false,
timezone: DateTime.local().zoneName,
useNotes: false,
usesNotes: false,
visibleDays: []
}),
getters: {
Expand Down Expand Up @@ -160,7 +160,7 @@ export const useAgendaStore = defineStore('agenda', {
this.isCurrentMeeting = agendaData.isCurrentMeeting
this.meeting = agendaData.meeting
this.schedule = agendaData.schedule
this.useNotes = agendaData.useNotes
this.usesNotes = agendaData.usesNotes

// -> Compute current info note hash
this.infoNoteHash = murmur(agendaData.meeting.infoNote, 0).toString()
Expand Down
80 changes: 80 additions & 0 deletions client/components/Status.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup>
import { h, onMounted } from 'vue'
import { useNotification } from 'naive-ui'
import { localStorageWrapper } from '../shared/local-storage-wrapper'
import { JSONWrapper } from '../shared/json-wrapper'
import { STATUS_STORAGE_KEY, generateStatusTestId } from '../shared/status-common'
const getDismissedStatuses = () => {
const jsonString = localStorageWrapper.getItem(STATUS_STORAGE_KEY)
const jsonValue = JSONWrapper.parse(jsonString, [])
if(Array.isArray(jsonValue)) {
return jsonValue
}
return []
}
const dismissStatus = (id) => {
const dissmissed = [id, ...getDismissedStatuses()]
localStorageWrapper.setItem(STATUS_STORAGE_KEY, JSONWrapper.stringify(dissmissed))
return true
}
let notificationInstances = {} // keyed by status.id
let notification
const pollStatusAPI = () => {
fetch('/status/latest.json')
.then(resp => resp.json())
.then(status => {
if(status === null || status.hasMessage === false) {
console.debug("No status message")
return
}
const dismissedStatuses = getDismissedStatuses()
if(dismissedStatuses.includes(status.id)) {
console.debug(`Not showing site status ${status.id} because it was already dismissed. Dismissed Ids:`, dismissedStatuses)
return
}
const isSameStatusPage = Boolean(document.querySelector(`[data-status-id="${status.id}"]`))
if(isSameStatusPage) {
console.debug(`Not showing site status ${status.id} because we're on the target page`)
return
}
if(notificationInstances[status.id]) {
console.debug(`Not showing site status ${status.id} because it's already been displayed`)
return
}
notificationInstances[status.id] = notification.create({
title: status.title,
content: status.body,
meta: `${status.date}`,
action: () =>
h(
'a',
{
'data-testid': generateStatusTestId(status.id),
href: status.url,
'aria-label': `Read more about ${status.title}`
},
"Read more"
),
onClose: () => {
return dismissStatus(status.id)
}
})
})
.catch(e => {
console.error(e)
})
}
onMounted(() => {
notification = useNotification()
pollStatusAPI(notification)
})
</script>
2 changes: 2 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<link href="https://static.ietf.org/fonts/noto-sans-mono/import.css" rel="stylesheet">
</head>
<body>
<div class="vue-embed" data-component="Status"></div>
<div class="pt-3 container-fluid">
<div class="row">
<div class="col mx-lg-3" id="content">
Expand All @@ -20,5 +21,6 @@
</div>
</div>
<script type="module" src="./main.js"></script>
<script type="module" src="./embedded.js"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions client/shared/json-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const JSONWrapper = {
parse(jsonString, defaultValue) {
if(typeof jsonString !== "string") {
return defaultValue
}
try {
return JSON.parse(jsonString);
} catch (e) {
console.error(e);
}
return defaultValue
},
stringify(data) {
try {
return JSON.stringify(data);
} catch (e) {
console.error(e)
}
},
}
42 changes: 42 additions & 0 deletions client/shared/local-storage-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

/*
* DEVELOPER NOTE
*
* Some browsers can block storage (localStorage, sessionStorage)
* access for privacy reasons, and all browsers can have storage
* that's full, and then they throw exceptions.
*
* See https://michalzalecki.com/why-using-localStorage-directly-is-a-bad-idea/
*
* Exceptions can even be thrown when testing if localStorage
* even exists. This can throw:
*
* if (window.localStorage)
*
* Also localStorage/sessionStorage can be enabled after DOMContentLoaded
* so we handle it gracefully.
*
* 1) we need to wrap all usage in try/catch
* 2) we need to defer actual usage of these until
* necessary,
*
*/

export const localStorageWrapper = {
getItem: (key) => {
try {
return localStorage.getItem(key)
} catch (e) {
console.error(e);
}
return null;
},
setItem: (key, value) => {
try {
return localStorage.setItem(key, value)
} catch (e) {
console.error(e);
}
return;
},
}
5 changes: 5 additions & 0 deletions client/shared/status-common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Used in Playwright Status and components

export const STATUS_STORAGE_KEY = "status-dismissed"

export const generateStatusTestId = (id) => `status-${id}`
Empty file added ietf/admin/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions ietf/admin/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright The IETF Trust 2024, All Rights Reserved
from django.contrib.admin import apps as admin_apps


class AdminConfig(admin_apps.AdminConfig):
default_site = "ietf.admin.sites.AdminSite"
15 changes: 15 additions & 0 deletions ietf/admin/sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright The IETF Trust 2024, All Rights Reserved
from django.contrib.admin import AdminSite as _AdminSite
from django.conf import settings
from django.utils.safestring import mark_safe


class AdminSite(_AdminSite):
site_title = "Datatracker admin"

@staticmethod
def site_header():
if settings.SERVER_MODE == "production":
return "Datatracker administration"
else:
return mark_safe('Datatracker administration <span class="text-danger">&delta;</span>')
41 changes: 16 additions & 25 deletions ietf/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

from urllib.parse import urlencode

from django.conf import settings
from django.apps import apps as django_apps
from django.core.exceptions import ObjectDoesNotExist
from django.utils.module_loading import autodiscover_modules


import debug # pyflakes:ignore

Expand All @@ -21,38 +23,27 @@

_api_list = []

for _app in settings.INSTALLED_APPS:
OMITTED_APPS_APIS = ["ietf.status"]

def populate_api_list():
_module_dict = globals()
if '.' in _app:
_root, _name = _app.split('.', 1)
if _root == 'ietf':
if not '.' in _name:
_api = Api(api_name=_name)
_module_dict[_name] = _api
_api_list.append((_name, _api))
for app_config in django_apps.get_app_configs():
if '.' in app_config.name and app_config.name not in OMITTED_APPS_APIS:
_root, _name = app_config.name.split('.', 1)
if _root == 'ietf':
if not '.' in _name:
_api = Api(api_name=_name)
_module_dict[_name] = _api
_api_list.append((_name, _api))

def autodiscover():
"""
Auto-discover INSTALLED_APPS resources.py modules and fail silently when
not present. This forces an import on them to register any admin bits they
not present. This forces an import on them to register any resources they
may want.
"""
autodiscover_modules("resources")

from importlib import import_module
from django.conf import settings
from django.utils.module_loading import module_has_submodule

for app in settings.INSTALLED_APPS:
mod = import_module(app)
# Attempt to import the app's admin module.
try:
import_module('%s.resources' % (app, ))
except:
# Decide whether to bubble up this error. If the app just
# doesn't have an admin module, we can ignore the error
# attempting to import it, otherwise we want it to bubble up.
if module_has_submodule(mod, "resources"):
raise

class ModelResource(tastypie.resources.ModelResource):
def generate_cache_key(self, *args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions ietf/api/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ class Serializer(): ...
class ToOneField(tastypie.fields.ToOneField): ...
class TimedeltaField(tastypie.fields.ApiField): ...

def populate_api_list() -> None: ...
def autodiscover() -> None: ...
15 changes: 15 additions & 0 deletions ietf/api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.apps import AppConfig
from . import populate_api_list


class ApiConfig(AppConfig):
name = "ietf.api"

def ready(self):
"""Hook to do init after the app registry is fully populated
Importing models or accessing the app registry is ok here, but do not
interact with the database. See
https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready
"""
populate_api_list()
1 change: 1 addition & 0 deletions ietf/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
'ietf.secr.meetings',
'ietf.secr.proceedings',
'ietf.ipr',
'ietf.status',
)

class CustomApiTests(TestCase):
Expand Down
1 change: 1 addition & 0 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ietf.submit import views as submit_views
from ietf.utils.urls import url


api.autodiscover()

urlpatterns = [
Expand Down
2 changes: 2 additions & 0 deletions ietf/doc/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ def generate_draft_bibxml_files_task(days=7, process_all=False):
If process_all is False (the default), processes only docs with new revisions
in the last specified number of days.
"""
if not process_all and days < 1:
raise ValueError("Must call with days >= 1 or process_all=True")
ensure_draft_bibxml_path_exists()
doc_events = NewRevisionDocEvent.objects.filter(
type="new_revision",
Expand Down
Loading

0 comments on commit 162aa54

Please sign in to comment.