Skip to content
Open
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
14 changes: 8 additions & 6 deletions seqr/views/apis/feature_updates_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@
TIMEOUT = 5


def fetch_feature_updates():
response = requests.get(FEED_URL, timeout=TIMEOUT)
response.raise_for_status()
feed = parse(response.content)
return feed.entries


def get_feature_updates(request):
"""
Fetches the feature-updates GitHub Discussion Atom feed, converts feed entries into markdown, and returns
markdown and information for each feed entry.
"""
response = requests.get(FEED_URL, timeout=TIMEOUT)
response.raise_for_status()

feed = parse(response.content)

entries = []
for entry in feed.entries:
for entry in fetch_feature_updates():
# Atom feeds can have multiple content elements per feed entry
markdown = "".join(md(content.value) for content in entry.content)
entries.append(
Expand Down
22 changes: 22 additions & 0 deletions seqr/views/react_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from django.middleware.csrf import rotate_token
from django.template import loader
from django.http import HttpResponse
import logging

from settings import (
SEQR_VERSION,
CSRF_COOKIE_NAME,
Expand All @@ -15,9 +17,13 @@
VLM_CLIENT_ID,
)
from seqr.models import WarningMessage
from seqr.utils.redis_utils import safe_redis_get_json, safe_redis_set_json
from seqr.views.apis.feature_updates_api import fetch_feature_updates
from seqr.views.utils.orm_to_json_utils import get_json_for_user, get_json_for_current_user
from seqr.views.utils.permissions_utils import login_active_required

logger = logging.getLogger(__name__)


@login_active_required(login_url=LOGIN_URL)
def main_app(request, *args, **kwargs):
Expand Down Expand Up @@ -56,6 +62,7 @@ def render_app_html(request, additional_json=None, include_user=True, status=200
'oauthLoginProvider': SOCIAL_AUTH_PROVIDER,
'vlmEnabled': bool(VLM_CLIENT_ID),
'warningMessages': [message.json() for message in WarningMessage.objects.all()],
'lastFeatureUpdate': _get_latest_feature_update_date(),
}}
if include_user:
initial_json['user'] = get_json_for_current_user(request.user)
Expand Down Expand Up @@ -89,3 +96,18 @@ def render_app_html(request, additional_json=None, include_user=True, status=200
)

return HttpResponse(html, content_type="text/html", status=status)


FEATURE_CACHE_KEY = 'feature_updates_latest_date'

def _get_latest_feature_update_date():
update_date = safe_redis_get_json(FEATURE_CACHE_KEY)
if not update_date:
try:
entries = fetch_feature_updates()
except Exception as e:
logger.error(f'Unable to fetch feature update date: {e}')
return None
update_date = entries[0].published
safe_redis_set_json(FEATURE_CACHE_KEY, update_date, expire=60*60*3)
return update_date
93 changes: 85 additions & 8 deletions seqr/views/react_app_tests.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,81 @@
from datetime import datetime
from django.urls.base import reverse
import mock
import responses

from seqr.views.react_app import main_app, no_login_main_app
from seqr.views.utils.terra_api_utils import TerraRefreshTokenFailedException
from seqr.views.utils.test_utils import TEST_OAUTH2_PROVIDER, AuthenticationTestCase, AnvilAuthenticationTestCase, USER_FIELDS

MOCK_GA_TOKEN = 'mock_ga_token' # nosec
FEATURE_UPDATE_FEED = """
<?xml version="1.0" ?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<id>tag:github.com,2008:/broadinstitute/seqr/discussions/categories/feature-updates</id>
<link type="text/html" rel="alternate" href="https://github.com/broadinstitute/seqr/discussions/categories/feature-updates"/>
<link type="application/atom+xml" rel="self" href="https://github.com/broadinstitute/seqr/discussions/categories/feature-updates.atom"/>
<title>Recent discussions in broadinstitute/seqr, category: feature-updates</title>
<updated>2023-11-08T15:11:40+00:00</updated>
<entry>
<id>tag:github.com,2008:5828412</id>
<link type="text/html" rel="alternate" href="https://github.com/broadinstitute/seqr/discussions/3713"/>
<title>Welcome to Feature Updates</title>
<published>2023-11-08T15:11:31+00:00</published>
<updated>2023-11-08T15:11:40+00:00</updated>
<media:thumbnail height="30" width="30" url="https://avatars.githubusercontent.com/u/24598672?s=30&amp;v=4"/>
<author>
<name>hanars</name>
<uri>https://github.com/hanars</uri>
</author>
<content type="html">
&lt;p dir=&quot;auto&quot;&gt;Welcome to the seqr feature update discussion channel! This channel will be used for all announcements of new seqr functionality&lt;/p&gt;
</content>
<content type="html">
&lt;h2 dir=&quot;auto&quot;&gt;Welcome to our discussion forum!&lt;/h2&gt;
&lt;p dir=&quot;auto&quot;&gt;We\xe2\x80\x99re using Discussions as a place to connect members of the seqr community with our development team and with one another. We hope that you:&lt;/p&gt;
&lt;ul dir=&quot;auto&quot;&gt;
&lt;li&gt;Ask questions&lt;/li&gt;
&lt;li&gt;Describe issues that may be user errors instead of bugs&lt;/li&gt;
&lt;li&gt;Answer and engage with one another&lt;/li&gt;
&lt;li&gt;Discuss ideas for enhancements&lt;/li&gt;
&lt;/ul&gt;
</content>
</entry>
<entry>
<id>tag:github.com,2008:3913526</id>
<link type="text/html" rel="alternate" href="https://github.com/broadinstitute/seqr/discussions/2501"/>
<title>[ACTION REQUIRED] Upcoming Deprecation of older docker images and static JS/CSS assets
</title>
<published>2022-03-03T16:25:56+00:00</published>
<updated>2022-06-23T13:32:55+00:00</updated>
<media:thumbnail height="30" width="30" url="https://avatars.githubusercontent.com/u/636687?s=30&amp;v=4"/>
<author>
<name>sjahl</name>
<uri>https://github.com/sjahl</uri>
</author>
<content type="html">
&lt;h3 dir=&quot;auto&quot;&gt;What is happening?&lt;/h3&gt;
&lt;h4 dir=&quot;auto&quot;&gt;Summary&lt;/h4&gt;
&lt;p dir=&quot;auto&quot;&gt;Seqr is making updates to how our docker images work, and you will need to update your deployment by 2022/04/04. Please see the information below for more details.&lt;/p&gt;
&lt;h4 dir=&quot;auto&quot;&gt;Slightly Longer Version&lt;/h4&gt;
&lt;p dir=&quot;auto&quot;&gt;We\xe2\x80\x99re making two functional changes to the way seqr is packaged in Docker.&lt;/p&gt;
&lt;ol dir=&quot;auto&quot;&gt;
&lt;li&gt;We will be removing the statically built javascript/CSS/HTML assets that are checked into git. These will now be generated in the docker image when it\xe2\x80\x99s built, rather than provided in the source code tree.&lt;/li&gt;
&lt;li&gt;New docker images will be built every time we release seqr. These images will no longer &lt;code class=&quot;notranslate&quot;&gt;git pull&lt;/code&gt; to update themselves on startup.&lt;/li&gt;
&lt;/ol&gt;
&lt;p dir=&quot;auto&quot;&gt;However, if you are using a docker image from prior to when we made these changes, your installation still has the &lt;code class=&quot;notranslate&quot;&gt;git pull&lt;/code&gt; update functionality. If you do nothing, your seqr installation may break after 2022/04/04 when it pulls down the version of the seqr code that has the assets removed. You will find instructions below on how to proceed.&lt;/p&gt;
</content>
</entry>
</feed>
"""


@mock.patch('seqr.views.react_app.DEBUG', False)
@mock.patch('seqr.utils.redis_utils.redis.StrictRedis')
class AppPageTest(object):
databases = ['default']
fixtures = ['users']

def _check_page_html(self, response, user, user_key='user', vlm_enabled=False, user_email=None, user_fields=None, ga_token_id=None):
def _check_page_html(self, response, user, user_key='user', vlm_enabled=False, user_email=None, user_fields=None, ga_token_id=None, last_feature_update=None):
user_fields = user_fields or USER_FIELDS
self.assertEqual(response.status_code, 200)
initial_json = self.get_initial_page_json(response)
Expand All @@ -26,6 +88,7 @@ def _check_page_html(self, response, user, user_key='user', vlm_enabled=False,
'oauthLoginProvider': self.OAUTH_PROVIDER,
'vlmEnabled': vlm_enabled,
'warningMessages': [{'id': 1, 'header': 'Warning!', 'message': 'A sample warning'}],
'lastFeatureUpdate': last_feature_update,
})

self.assertEqual(self.get_initial_page_window('gaTrackingId', response), ga_token_id)
Expand All @@ -39,14 +102,24 @@ def _check_page_html(self, response, user, user_key='user', vlm_enabled=False,

@mock.patch('seqr.views.react_app.VLM_CLIENT_ID', 'abc123')
@mock.patch('seqr.views.react_app.GA_TOKEN_ID', MOCK_GA_TOKEN)
def test_react_page(self):
@responses.activate
def test_react_page(self, mock_redis):
url = reverse(main_app)
self.check_require_login_no_policies(url, login_redirect_url='/login')

responses.add(
responses.GET,
'https://github.com/broadinstitute/seqr/discussions/categories/feature-updates.atom',
body=FEATURE_UPDATE_FEED,
)
response = self.client.get(url)
self._check_page_html(response, 'test_user_no_policies', user_email='test_user_no_policy@test.com', ga_token_id=MOCK_GA_TOKEN, vlm_enabled=True)
self._check_page_html(response, 'test_user_no_policies', user_email='test_user_no_policy@test.com', ga_token_id=MOCK_GA_TOKEN, vlm_enabled=True, last_feature_update='2023-11-08T15:11:31+00:00')

def test_local_react_page(self):
mock_redis.return_value.set.assert_called_with('feature_updates_latest_date', '"2023-11-08T15:11:31+00:00"')
mock_redis.return_value.expire.assert_called_with('feature_updates_latest_date', 10800)

@responses.activate
def test_local_react_page(self, mock_redis):
url = reverse(no_login_main_app)
response = self.client.get(url, HTTP_HOST='localhost:3000')
self.assertEqual(response.status_code, 200)
Expand All @@ -56,7 +129,8 @@ def test_local_react_page(self):
self.assertContains(response, 'src="/app.js"')
self.assertNotRegex(content, r'<link\s+href="/static/app.*css"[^>]*>')

def test_no_login_react_page(self):
@responses.activate
def test_no_login_react_page(self, mock_redis):
url = reverse(no_login_main_app)

response = self.client.get(url)
Expand All @@ -81,12 +155,15 @@ def test_no_login_react_page(self):
response = self.client.get(url)
self._check_page_html(response, 'test_user')

def test_react_page_additional_configs(self):
@responses.activate
def test_react_page_additional_configs(self, mock_redis):
url = reverse(main_app)
self.check_require_login_no_policies(url, login_redirect_url='/login')

mock_redis.return_value.get.return_value = '"2025-11-18T00:00:00Z"'

response = self.client.get(url)
self._check_page_html(response, 'test_user_no_policies')
self._check_page_html(response, 'test_user_no_policies', last_feature_update='2025-11-18T00:00:00Z')


class LocalAppPageTest(AuthenticationTestCase, AppPageTest):
Expand Down
1 change: 1 addition & 0 deletions ui/redux/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const getOauthLoginProvider = state => state.meta.oauthLoginProvider
export const getVlmEnabled = state => state.meta.vlmEnabled
export const getHijakEnabled = state => state.meta.hijakEnabled
export const getWarningMessages = state => state.meta.warningMessages
export const getLastFeatureUpdate = state => state.meta.lastFeatureUpdate
export const getSavedVariantsIsLoading = state => state.savedVariantsLoading.isLoading
export const getSavedVariantsLoadingError = state => state.savedVariantsLoading.errorMessage
export const getSearchesByHash = state => state.searchesByHash
Expand Down
14 changes: 10 additions & 4 deletions ui/shared/components/page/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'

import { Menu, Header, Dropdown } from 'semantic-ui-react'
import { Menu, Header, Dropdown, Label } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'

import { updateUser } from 'redux/rootReducer'
import { getUser, getOauthLoginProvider } from 'redux/selectors'
import { getUser, getOauthLoginProvider, getLastFeatureUpdate } from 'redux/selectors'
import { USER_NAME_FIELDS, LOCAL_LOGIN_URL, FEATURE_UPDATES_PATH } from 'shared/utils/constants'
import UpdateButton from '../buttons/UpdateButton'

Expand All @@ -18,7 +18,7 @@ const HeaderMenu = styled(Menu)`
padding-right: 100px;
`

const PageHeader = React.memo(({ user, oauthLoginProvider, onSubmit }) => {
const PageHeader = React.memo(({ user, oauthLoginProvider, onSubmit, lastFeatureUpdate }) => {
const loginUrl = oauthLoginProvider ? `/login/${oauthLoginProvider}` : LOCAL_LOGIN_URL

return (
Expand All @@ -34,7 +34,11 @@ const PageHeader = React.memo(({ user, oauthLoginProvider, onSubmit }) => {
<Menu.Item key="awesomebar" fitted="vertically"><AwesomeBar newWindow inputwidth="350px" /></Menu.Item>,
] : null }
<Menu.Item key="spacer" position="right" />
<Menu.Item key="feature_updates" as={Link} to={FEATURE_UPDATES_PATH} content="Feature Updates" />
<Menu.Item key="feature_updates">
<Link to={FEATURE_UPDATES_PATH}>Feature Updates</Link>
{(lastFeatureUpdate && (new Date()).setMonth(new Date().getMonth() - 1) < new Date(lastFeatureUpdate)) &&
<Label color="red" pointing="left" size="tiny">New</Label>}
</Menu.Item>
{Object.keys(user).length ? [
<Dropdown
item
Expand Down Expand Up @@ -68,12 +72,14 @@ PageHeader.propTypes = {
user: PropTypes.object,
oauthLoginProvider: PropTypes.string,
onSubmit: PropTypes.func,
lastFeatureUpdate: PropTypes.string,
}

// wrap top-level component so that redux state is passed in as props
const mapStateToProps = state => ({
user: getUser(state),
oauthLoginProvider: getOauthLoginProvider(state),
lastFeatureUpdate: getLastFeatureUpdate(state),
})

const mapDispatchToProps = {
Expand Down
Loading