Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Site status message #7659

Merged
merged 31 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d03fd2e
Status WIP
Jun 20, 2024
933f0be
feat: Status
Jul 9, 2024
04ab0ba
Merge branch 'main' into status-message
Jul 9, 2024
175aea5
fix: Status tests
Jul 10, 2024
6408b65
feat: status redirect
Jul 10, 2024
4f47554
chore: Status tests
Jul 10, 2024
cd5f9ae
chore: Status tests
Jul 10, 2024
1a338bb
feat: Status tests
Jul 11, 2024
d6b2eee
chore: Status playwright tests
Jul 15, 2024
7e98341
fix: PR feedback, mostly Vue and copyright dates
Jul 15, 2024
5c10968
fix: Status model migration tidy up
Jul 15, 2024
42ed840
chore: Status - one migration
Jul 16, 2024
5bc8145
feat: status on doc/html pages
Jul 21, 2024
174fdd8
chore: merging main. Resolving conflict in ietf.scss
Jul 21, 2024
7a0c02b
chore: Resetting Status migration
Jul 21, 2024
d207c1d
chore: removing unused FieldError
Jul 22, 2024
b64d267
fix: Update Status test to remove 'by'
Jul 22, 2024
3423355
chore: fixing API test to exclude 'status'
holloway Jul 22, 2024
ad8d265
chore: fixing status_page test
holloway Jul 23, 2024
93c986e
feat: Site Status PR feedback. URL coverage debugging
holloway Jul 25, 2024
7368ae1
Adding ietf.status to Tastypie omitted apps
holloway Jul 25, 2024
09c87c0
feat: Site Status PR feedback
holloway Jul 26, 2024
9db7147
Merge branch 'main' into feat/status-message
holloway Jul 26, 2024
93eb0f0
chore: correct copyright year on newly created files
holloway Jul 30, 2024
2c9c897
Merge branch 'feat/status-message' of gh-ietf:ietf-tools/datatracker …
holloway Jul 30, 2024
dfa3732
Merge branch 'main' into feat/status-message
rjsparks Aug 1, 2024
b21661c
Merge remote-tracking branch 'ietf-tools/main' into feat/status-message
rjsparks Aug 7, 2024
04cef0f
chore: repair merge damage
rjsparks Aug 7, 2024
a7e627f
Merge branch 'main' into feat/status-message
rjsparks Aug 7, 2024
de8a991
chore: repair more merge damage
rjsparks Aug 7, 2024
e7ef7f2
fix: reconcile the api init refactor with ignoring apps
rjsparks Aug 7, 2024
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
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
80 changes: 80 additions & 0 deletions client/components/Status.vue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various uses of semi-colons. Remove all.

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}`
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/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ def skip_unreadable_post(record):
'ietf.release',
'ietf.review',
'ietf.stats',
'ietf.status',
'ietf.submit',
'ietf.sync',
'ietf.utils',
Expand Down
7 changes: 7 additions & 0 deletions ietf/static/css/ietf.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,13 @@ blockquote {
border-left: solid 1px var(--bs-body-color);
}

iframe.status {
background-color:transparent;
border:none;
width:100%;
height:3.5em;
}

.overflow-shadows {
transition: box-shadow 0.5s;
}
Expand Down
Empty file added ietf/status/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions ietf/status/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright The IETF Trust 2017-2024, All Rights Reserved
# -*- coding: utf-8 -*-

from datetime import datetime
from django.contrib import admin
from django.template.defaultfilters import slugify
from .models import Status

class StatusAdmin(admin.ModelAdmin):
list_display = ['title', 'body', 'active', 'date', 'by', 'page']
raw_id_fields = ['by']

def get_changeform_initial_data(self, request):
date = datetime.now()
return {
"slug": slugify(f"{date.year}-{date.month}-{date.day}-"),
}

admin.site.register(Status, StatusAdmin)
9 changes: 9 additions & 0 deletions ietf/status/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright The IETF Trust 2017-2024, All Rights Reserved
# -*- coding: utf-8 -*-

from django.apps import AppConfig


class StatusConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "ietf.status"
75 changes: 75 additions & 0 deletions ietf/status/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Generated by Django 4.2.13 on 2024-07-21 22:47

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
("person", "0002_alter_historicalperson_ascii_and_more"),
]

operations = [
migrations.CreateModel(
name="Status",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(default=django.utils.timezone.now)),
("slug", models.SlugField(unique=True)),
(
"title",
models.CharField(
help_text="Your site status notification title.",
max_length=255,
verbose_name="Status title",
),
),
(
"body",
models.CharField(
help_text="Your site status notification body.",
max_length=255,
verbose_name="Status body",
),
),
(
"active",
models.BooleanField(
default=True,
help_text="Only active messages will be shown.",
verbose_name="Active?",
),
),
(
"page",
models.TextField(
blank=True,
help_text="More detail shown after people click 'Read more'. If empty no 'read more' will be shown",
null=True,
verbose_name="More detail (markdown)",
),
),
(
"by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="person.person"
),
),
],
options={
"verbose_name_plural": "statuses",
},
),
]
Empty file.
24 changes: 24 additions & 0 deletions ietf/status/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright The IETF Trust 2017-2024, All Rights Reserved
# -*- coding: utf-8 -*-

from django.utils import timezone
from django.db import models
from django.db.models import ForeignKey

import debug # pyflakes:ignore

class Status(models.Model):
name = 'Status'

date = models.DateTimeField(default=timezone.now)
slug = models.SlugField(blank=False, null=False, unique=True)
title = models.CharField(max_length=255, verbose_name="Status title", help_text="Your site status notification title.")
body = models.CharField(max_length=255, verbose_name="Status body", help_text="Your site status notification body.", unique=False)
active = models.BooleanField(default=True, verbose_name="Active?", help_text="Only active messages will be shown.")
by = ForeignKey('person.Person', on_delete=models.CASCADE)
page = models.TextField(blank=True, null=True, verbose_name="More detail (markdown)", help_text="More detail shown after people click 'Read more'. If empty no 'read more' will be shown")

def __str__(self):
return "{} {} {} {}".format(self.date, self.active, self.by, self.title)
class Meta:
verbose_name_plural = "statuses"
Loading
Loading