diff --git a/api/migrations/versions/99a3d2eb08f9_add_user_display_name.py b/api/migrations/versions/99a3d2eb08f9_add_user_display_name.py
new file mode 100644
index 0000000..9d6a637
--- /dev/null
+++ b/api/migrations/versions/99a3d2eb08f9_add_user_display_name.py
@@ -0,0 +1,26 @@
+"""add user display_name
+
+Revision ID: 99a3d2eb08f9
+Revises: a9627f63fbed
+Create Date: 2022-09-13 07:30:18.747880
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "99a3d2eb08f9"
+down_revision = "a9627f63fbed"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.add_column(
+ "user", sa.Column("display_name", sa.String, nullable=True), schema="public"
+ )
+
+
+def downgrade():
+ op.drop_column("user", "display_name", schema="public")
diff --git a/api/obs/api/db.py b/api/obs/api/db.py
index 10dbbb3..64d8e50 100644
--- a/api/obs/api/db.py
+++ b/api/obs/api/db.py
@@ -354,6 +354,7 @@ class User(Base):
updated_at = Column(DateTime, nullable=False, server_default=NOW, onupdate=NOW)
sub = Column(String, unique=True, nullable=False)
username = Column(String, unique=True, nullable=False)
+ display_name = Column(String, nullable=True)
email = Column(String, nullable=False)
bio = Column(TEXT)
image = Column(String)
@@ -374,11 +375,15 @@ class User(Base):
self.api_key = secrets.token_urlsafe(24)
def to_dict(self, for_user_id=None):
- return {
- "username": self.username,
+ result = {
+ "id": self.id,
+ "displayName": self.display_name or self.username,
"bio": self.bio,
"image": self.image,
}
+ if for_user_id == self.id:
+ result["username"] = self.username
+ return result
async def rename(self, config, new_name):
old_name = self.username
diff --git a/api/obs/api/routes/tiles.py b/api/obs/api/routes/tiles.py
index 1f5771a..9b6b652 100644
--- a/api/obs/api/routes/tiles.py
+++ b/api/obs/api/routes/tiles.py
@@ -66,12 +66,9 @@ def get_filter_options(
* start (datetime|None)
* end (datetime|None)
"""
- user_id = None
- username = req.ctx.get_single_arg("user", default=None)
- if username is not None:
- if req.ctx.user is None or req.ctx.user.username != username:
- raise Forbidden()
- user_id = req.ctx.user.id
+ user_id = req.ctx.get_single_arg("user", default=None, convert=int)
+ if user_id is not None and (req.ctx.user is None or req.ctx.user.id != user_id):
+ raise Forbidden()
parse_date = lambda s: dateutil.parser.parse(s)
start = req.ctx.get_single_arg("start", default=None, convert=parse_date)
diff --git a/api/obs/api/routes/tracks.py b/api/obs/api/routes/tracks.py
index d0c4425..8ae1c6a 100644
--- a/api/obs/api/routes/tracks.py
+++ b/api/obs/api/routes/tracks.py
@@ -63,13 +63,13 @@ async def _return_tracks(req, extend_query, limit, offset):
async def get_tracks(req):
limit = req.ctx.get_single_arg("limit", default=20, convert=int)
offset = req.ctx.get_single_arg("offset", default=0, convert=int)
- author = req.ctx.get_single_arg("author", default=None, convert=str)
+ # author = req.ctx.get_single_arg("author", default=None, convert=int)
def extend_query(q):
q = q.where(Track.public)
- if author is not None:
- q = q.where(User.username == author)
+ # if author is not None:
+ # q = q.where(Track.author_id == author)
return q
diff --git a/api/obs/api/routes/users.py b/api/obs/api/routes/users.py
index 2c86e3d..60b0c19 100644
--- a/api/obs/api/routes/users.py
+++ b/api/obs/api/routes/users.py
@@ -12,7 +12,9 @@ from obs.api import __version__ as version
def user_to_json(user):
return {
+ "id": user.id,
"username": user.username,
+ "displayName": user.display_name,
"email": user.email,
"bio": user.bio,
"image": user.image,
@@ -36,6 +38,9 @@ async def put_user(req):
if key in data and isinstance(data[key], (str, type(None))):
setattr(user, key, data[key])
+ if "displayName" in data:
+ user.display_name = data["displayName"] or None
+
if "areTracksVisibleForAll" in data:
user.are_tracks_visible_for_all = bool(data["areTracksVisibleForAll"])
diff --git a/frontend/src/components/Avatar/index.tsx b/frontend/src/components/Avatar/index.tsx
index a2d49f5..72f85eb 100644
--- a/frontend/src/components/Avatar/index.tsx
+++ b/frontend/src/components/Avatar/index.tsx
@@ -1,39 +1,42 @@
-import React from 'react'
-import {Comment} from 'semantic-ui-react'
-import classnames from 'classnames'
+import React from "react";
+import { Comment } from "semantic-ui-react";
+import classnames from "classnames";
-import './styles.less'
+import "./styles.less";
function hashCode(s) {
- let hash = 0
+ let hash = 0;
for (let i = 0; i < s.length; i++) {
- hash = (hash << 5) - hash + s.charCodeAt(i)
- hash |= 0
+ hash = (hash << 5) - hash + s.charCodeAt(i);
+ hash |= 0;
}
- return hash
+ return hash;
}
function getColor(s) {
- const h = Math.floor(hashCode(s)) % 360
- return `hsl(${h}, 50%, 50%)`
+ const h = Math.floor(hashCode(s)) % 360;
+ return `hsl(${h}, 50%, 50%)`;
}
-export default function Avatar({user, className}) {
- const {image, username} = user || {}
+export default function Avatar({ user, className }) {
+ const { image, displayName } = user || {};
if (image) {
- return
+ return ;
}
- if (!username) {
- return
+ if (!displayName) {
+ return ;
}
- const color = getColor(username)
+ const color = getColor(displayName);
return (
-
- {username &&
{username[0]}}
+
+ {displayName && {displayName[0]}}
- )
+ );
}
diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx
index 7b37d7d..921130c 100644
--- a/frontend/src/pages/MapPage/index.tsx
+++ b/frontend/src/pages/MapPage/index.tsx
@@ -187,7 +187,7 @@ function MapPage({ login }) {
const query = new URLSearchParams();
if (login) {
if (mapConfig.filters.currentUser) {
- query.append("user", login.username);
+ query.append("user", login.id);
}
if (mapConfig.filters.dateMode === "range") {
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
index c0b9bb5..11be6e5 100644
--- a/frontend/src/pages/SettingsPage.tsx
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -68,18 +68,37 @@ const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
{t("SettingsPage.profile.title")}
- {t("SettingsPage.profile.publicNotice")}
-
+
+ [
+
+ ]
+ {t("SettingsPage.profile.username.hint")}
+
+
+
+ {t("SettingsPage.profile.publicNotice")}
+
+
+
+
+ [
+
+ ]
+
+ {t("SettingsPage.profile.displayName.fallbackNotice")}
+
+
+
[
@@ -103,7 +122,7 @@ const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
-
+
]
@@ -140,7 +159,7 @@ function CopyInput({ value, ...props }) {
}
position="top right"
open={success != null}
- content={success ? t('general.copied') : t('general.copyError')}
+ content={success ? t("general.copied") : t("general.copyError")}
/>
);
}
diff --git a/frontend/src/pages/TrackEditor.tsx b/frontend/src/pages/TrackEditor.tsx
index 393ec39..0880f1c 100644
--- a/frontend/src/pages/TrackEditor.tsx
+++ b/frontend/src/pages/TrackEditor.tsx
@@ -76,7 +76,7 @@ const TrackEditor = connect((state) => ({ login: state.login }))(
);
const loading = busy || track == null;
- const isAuthor = login?.username === track?.author?.username;
+ const isAuthor = login?.id === track?.author?.id;
// Navigate to track detials if we are not the author
React.useEffect(() => {
diff --git a/frontend/src/pages/TrackPage/TrackComments.tsx b/frontend/src/pages/TrackPage/TrackComments.tsx
index 4f221f5..5c285b6 100644
--- a/frontend/src/pages/TrackPage/TrackComments.tsx
+++ b/frontend/src/pages/TrackPage/TrackComments.tsx
@@ -60,7 +60,9 @@ export default function TrackComments({
- {comment.author.username}
+
+ {comment.author.displayName}
+
@@ -69,7 +71,7 @@ export default function TrackComments({
{comment.body}
- {login?.username === comment.author.username && (
+ {login?.id === comment.author.id && (
{
@@ -77,7 +79,7 @@ export default function TrackComments({
e.preventDefault();
}}
>
- {t('general.delete')}
+ {t("general.delete")}
)}
diff --git a/frontend/src/pages/TrackPage/index.tsx b/frontend/src/pages/TrackPage/index.tsx
index b317c03..d7bc8d4 100644
--- a/frontend/src/pages/TrackPage/index.tsx
+++ b/frontend/src/pages/TrackPage/index.tsx
@@ -246,7 +246,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
[slug]
);
- const isAuthor = login?.username === data?.track?.author?.username;
+ const isAuthor = login?.id === data?.track?.author?.id;
const { track, trackData, comments } = data || {};
@@ -355,7 +355,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
onConfirm={hideDownloadError}
header={t("TrackPage.downloadFailed")}
content={String(downloadError)}
- confirmButton={t('general.ok')}
+ confirmButton={t("general.ok")}
/>
);
diff --git a/frontend/src/pages/TracksPage.tsx b/frontend/src/pages/TracksPage.tsx
index 033d6f2..68e3a80 100644
--- a/frontend/src/pages/TracksPage.tsx
+++ b/frontend/src/pages/TracksPage.tsx
@@ -1,49 +1,65 @@
-import React, {useCallback} from 'react'
-import {connect} from 'react-redux'
-import {Button, Message, Item, Header, Loader, Pagination, Icon} from 'semantic-ui-react'
-import {useObservable} from 'rxjs-hooks'
-import {Link} from 'react-router-dom'
-import {of, from, concat} from 'rxjs'
-import {map, switchMap, distinctUntilChanged} from 'rxjs/operators'
-import _ from 'lodash'
-import {useTranslation, Trans as Translate} from 'react-i18next'
+import React, { useCallback } from "react";
+import { connect } from "react-redux";
+import {
+ Button,
+ Message,
+ Item,
+ Header,
+ Loader,
+ Pagination,
+ Icon,
+} from "semantic-ui-react";
+import { useObservable } from "rxjs-hooks";
+import { Link } from "react-router-dom";
+import { of, from, concat } from "rxjs";
+import { map, switchMap, distinctUntilChanged } from "rxjs/operators";
+import _ from "lodash";
+import { useTranslation, Trans as Translate } from "react-i18next";
-import type {Track} from 'types'
-import {Avatar, Page, StripMarkdown, FormattedDate, Visibility} from 'components'
-import api from 'api'
-import {useQueryParam} from 'query'
+import type { Track } from "types";
+import {
+ Avatar,
+ Page,
+ StripMarkdown,
+ FormattedDate,
+ Visibility,
+} from "components";
+import api from "api";
+import { useQueryParam } from "query";
-function TrackList({privateTracks}: {privateTracks: boolean}) {
- const [page, setPage] = useQueryParam
('page', 1, Number)
+function TrackList({ privateTracks }: { privateTracks: boolean }) {
+ const [page, setPage] = useQueryParam("page", 1, Number);
- const pageSize = 10
+ const pageSize = 10;
const data: {
- tracks: Track[]
- trackCount: number
+ tracks: Track[];
+ trackCount: number;
} | null = useObservable(
(_$, inputs$) =>
inputs$.pipe(
map(([page, privateTracks]) => {
- const url = '/tracks' + (privateTracks ? '/feed' : '')
- const query = {limit: pageSize, offset: pageSize * (page - 1)}
- return {url, query}
+ const url = "/tracks" + (privateTracks ? "/feed" : "");
+ const query = { limit: pageSize, offset: pageSize * (page - 1) };
+ return { url, query };
}),
distinctUntilChanged(_.isEqual),
- switchMap((request) => concat(of(null), from(api.get(request.url, {query: request.query}))))
+ switchMap((request) =>
+ concat(of(null), from(api.get(request.url, { query: request.query })))
+ )
),
null,
[page, privateTracks]
- )
+ );
- const {tracks, trackCount} = data || {tracks: [], trackCount: 0}
- const loading = !data
- const totalPages = Math.ceil(trackCount / pageSize)
- const {t} = useTranslation()
+ const { tracks, trackCount } = data || { tracks: [], trackCount: 0 };
+ const loading = !data;
+ const totalPages = Math.ceil(trackCount / pageSize);
+ const { t } = useTranslation();
return (
-
+
{!loading && totalPages > 1 && (
{tracks.map((track: Track) => (
-
+
))}
) : (
)}
- )
+ );
}
export function NoPublicTracksMessage() {
@@ -72,27 +88,27 @@ export function NoPublicTracksMessage() {
No public tracks yet. Upload the first!
- )
+ );
}
-function maxLength(t: string|null, max: number): string|null {
+function maxLength(t: string | null, max: number): string | null {
if (t && t.length > max) {
- return t.substring(0, max) + ' ...'
+ return t.substring(0, max) + " ...";
} else {
- return t
+ return t;
}
}
const COLOR_BY_STATUS = {
- error: 'red',
- complete: 'green',
- created: 'gray',
- queued: 'orange',
- processing: 'orange',
-}
+ error: "red",
+ complete: "green",
+ created: "gray",
+ queued: "orange",
+ processing: "orange",
+};
-export function TrackListItem({track, privateTracks = false}) {
- const {t} = useTranslation()
+export function TrackListItem({ track, privateTracks = false }) {
+ const { t } = useTranslation();
return (
-
@@ -101,10 +117,14 @@ export function TrackListItem({track, privateTracks = false}) {
- {track.title || t('general.unnamedTrack')}
+ {track.title || t("general.unnamedTrack")}
- {privateTracks ? null : {t('TracksPage.createdBy', {author: track.author.username})}}
+ {privateTracks ? null : (
+
+ {t("TracksPage.createdBy", { author: track.author.displayName })}
+
+ )}
@@ -116,45 +136,57 @@ export function TrackListItem({track, privateTracks = false}) {
-
-
- {' '}
+
+ {" "}
{t(`TracksPage.processing.${track.processingStatus}`)}
)}
- )
+ );
}
-function UploadButton({navigate, ...props}) {
- const {t} = useTranslation()
+function UploadButton({ navigate, ...props }) {
+ const { t } = useTranslation();
const onClick = useCallback(
(e) => {
- e.preventDefault()
- navigate()
+ e.preventDefault();
+ navigate();
},
[navigate]
- )
+ );
return (
-