Replaced Gravatar with self-hosted avatars
Avatars are now obtained from Blender ID. They are downloaded from Blender ID and stored in the users' home project storage. Avatars can be synced via Celery and triggered from a webhook. The avatar can be obtained from the current user object in Python, or via pillar.api.users.avatar.url(user_dict). Avatars can be shown in the web frontend by: - an explicit image (like before but with a non-Gravatar URL) - a Vue.js component `user-avatar` - a Vue.js component `current-user-avatar` The latter is the most efficient for the current user, as it uses user info that's already injected into the webpage (so requires no extra queries).
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
export const UserEvents = {
|
||||
USER_LOADED: 'user-loaded',
|
||||
}
|
||||
let currentUserEventBus = new Vue();
|
||||
|
||||
class User{
|
||||
constructor(kwargs) {
|
||||
this.user_id = kwargs['user_id'] || '';
|
||||
this.username = kwargs['username'] || '';
|
||||
this.full_name = kwargs['full_name'] || '';
|
||||
this.gravatar = kwargs['gravatar'] || '';
|
||||
this.avatar_url = kwargs['avatar_url'] || '';
|
||||
this.email = kwargs['email'] || '';
|
||||
this.capabilities = kwargs['capabilities'] || [];
|
||||
this.badges_html = kwargs['badges_html'] || '';
|
||||
@@ -12,7 +17,7 @@ class User{
|
||||
|
||||
/**
|
||||
* """Returns True iff the user has one or more of the given capabilities."""
|
||||
* @param {...String} args
|
||||
* @param {...String} args
|
||||
*/
|
||||
hasCap(...args) {
|
||||
for(let cap of args) {
|
||||
@@ -25,10 +30,16 @@ class User{
|
||||
let currentUser;
|
||||
function initCurrentUser(kwargs){
|
||||
currentUser = new User(kwargs);
|
||||
currentUserEventBus.$emit(UserEvents.USER_LOADED, currentUser);
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
export { getCurrentUser, initCurrentUser }
|
||||
function updateCurrentUser(user) {
|
||||
currentUser = user;
|
||||
currentUserEventBus.$emit(UserEvents.USER_LOADED, currentUser);
|
||||
}
|
||||
|
||||
export { getCurrentUser, initCurrentUser, updateCurrentUser, currentUserEventBus }
|
||||
|
@@ -1,6 +1,6 @@
|
||||
export { transformPlaceholder } from './placeholder'
|
||||
export { prettyDate } from './prettydate'
|
||||
export { getCurrentUser, initCurrentUser } from './currentuser'
|
||||
export { getCurrentUser, initCurrentUser, updateCurrentUser, currentUserEventBus, UserEvents } from './currentuser'
|
||||
export { thenLoadImage } from './files'
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function debounced(fn, delay=1000) {
|
||||
|
||||
/**
|
||||
* Extracts error message from error of type String, Error or xhrError
|
||||
* @param {*} err
|
||||
* @param {*} err
|
||||
* @returns {String}
|
||||
*/
|
||||
export function messageFromError(err){
|
||||
|
@@ -19,6 +19,7 @@ import { StatusFilter } from './table/rows/filter/StatusFilter'
|
||||
import { TextFilter } from './table/rows/filter/TextFilter'
|
||||
import { NameFilter } from './table/rows/filter/NameFilter'
|
||||
import { UserAvatar } from './user/Avatar'
|
||||
import './user/CurrentUserAvatar'
|
||||
|
||||
let mixins = {
|
||||
UnitOfWorkTracker,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const TEMPLATE = `
|
||||
<div class="user-avatar">
|
||||
<img
|
||||
:src="user.gravatar"
|
||||
:src="user.avatar_url"
|
||||
:alt="user.full_name">
|
||||
</div>
|
||||
`;
|
||||
|
@@ -0,0 +1,23 @@
|
||||
const TEMPLATE = `
|
||||
<img class="user-avatar" :src="avatarUrl" alt="Your avatar">
|
||||
`
|
||||
|
||||
export let CurrentUserAvatar = Vue.component("current-user-avatar", {
|
||||
data: function() { return {
|
||||
avatarUrl: "",
|
||||
}},
|
||||
template: TEMPLATE,
|
||||
created: function() {
|
||||
pillar.utils.currentUserEventBus.$on(pillar.utils.UserEvents.USER_LOADED, this.updateAvatarURL);
|
||||
this.updateAvatarURL(pillar.utils.getCurrentUser());
|
||||
},
|
||||
methods: {
|
||||
updateAvatarURL(user) {
|
||||
if (typeof user === 'undefined') {
|
||||
this.avatarUrl = '';
|
||||
return;
|
||||
}
|
||||
this.avatarUrl = user.avatar_url;
|
||||
},
|
||||
},
|
||||
});
|
39
src/scripts/js/es6/individual/avatar/AvatarSync.js
Normal file
39
src/scripts/js/es6/individual/avatar/AvatarSync.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// The <i> is given a fixed width so that the button doesn't resize when we change the icon.
|
||||
const TEMPLATE = `
|
||||
<button class="btn btn-outline-primary" type="button" @click="syncAvatar"
|
||||
:disabled="isSyncing">
|
||||
<i style="width: 2em; display: inline-block"
|
||||
:class="{'pi-refresh': !isSyncing, 'pi-spin': isSyncing, spin: isSyncing}"></i>
|
||||
Fetch Avatar from Blender ID
|
||||
</button>
|
||||
`
|
||||
|
||||
Vue.component("avatar-sync-button", {
|
||||
template: TEMPLATE,
|
||||
data() { return {
|
||||
isSyncing: false,
|
||||
}},
|
||||
methods: {
|
||||
syncAvatar() {
|
||||
this.isSyncing = true;
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: `/settings/profile/sync-avatar`,
|
||||
})
|
||||
.then(response => {
|
||||
toastr.info("sync was OK");
|
||||
|
||||
let user = pillar.utils.getCurrentUser();
|
||||
user.avatar_url = response;
|
||||
pillar.utils.updateCurrentUser(user);
|
||||
})
|
||||
.catch(err => {
|
||||
toastr.error(xhrErrorResponseMessage(err), "There was an error syncing your avatar");
|
||||
})
|
||||
.then(() => {
|
||||
this.isSyncing = false;
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
1
src/scripts/js/es6/individual/avatar/init.js
Normal file
1
src/scripts/js/es6/individual/avatar/init.js
Normal file
@@ -0,0 +1 @@
|
||||
export { AvatarSync } from './AvatarSync';
|
@@ -101,3 +101,9 @@
|
||||
color: $color-success
|
||||
&.fail
|
||||
color: $color-danger
|
||||
|
||||
img.user-avatar
|
||||
border-radius: 1em
|
||||
box-shadow: 0 0 0 0.2em $color-background-light
|
||||
height: 160px
|
||||
width: 160px
|
||||
|
@@ -4,9 +4,9 @@
|
||||
li.dropdown
|
||||
| {% block menu_avatar %}
|
||||
a.navbar-item.dropdown-toggle(href="#", data-toggle="dropdown", title="{{ current_user.email }}")
|
||||
img.gravatar(
|
||||
src="{{ current_user.gravatar }}",
|
||||
alt="Avatar")
|
||||
current-user-avatar
|
||||
script.
|
||||
new Vue({el: 'current-user-avatar'})
|
||||
| {% endblock menu_avatar %}
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
|
@@ -165,7 +165,7 @@ h4 Organization members
|
||||
| {% for email in organization.unknown_members %}
|
||||
li.sharing-users-item.unknown-member(data-user-email='{{ email }}')
|
||||
.sharing-users-avatar
|
||||
img(src="{{ email | gravatar }}")
|
||||
img(src="{{ url_for('static_pillar', filename='assets/img/default_user_avatar.png') }}")
|
||||
.sharing-users-details
|
||||
span.sharing-users-email {{ email }}
|
||||
.sharing-users-action
|
||||
|
@@ -19,7 +19,7 @@
|
||||
user-id="{{ user['_id'] }}",
|
||||
class="{% if current_user.objectid == user['_id'] %}self{% endif %}")
|
||||
.sharing-users-avatar
|
||||
img(src="{{ user['avatar'] }}")
|
||||
img(src="{{ user['avatar_url'] }}")
|
||||
.sharing-users-details
|
||||
span.sharing-users-name
|
||||
| {{user['full_name']}}
|
||||
|
@@ -21,38 +21,50 @@ style.
|
||||
| {% block settings_page_content %}
|
||||
.settings-form
|
||||
form#settings-form(method='POST', action="{{url_for('settings.profile')}}")
|
||||
.pb-3
|
||||
.form-group
|
||||
.row
|
||||
.form-group.col-md-6
|
||||
| {{ form.username.label }}
|
||||
| {{ form.username(size=20, class='form-control') }}
|
||||
| {% if form.username.errors %}
|
||||
| {% for error in form.username.errors %}{{ error|e }}{% endfor %}
|
||||
| {% endif %}
|
||||
|
||||
.form-group
|
||||
label {{ _("Full name") }}
|
||||
p {{ current_user.full_name }}
|
||||
.form-group
|
||||
label {{ _("E-mail") }}
|
||||
p {{ current_user.email }}
|
||||
button.mt-3.btn.btn-outline-success.px-5.button-submit(type='submit')
|
||||
i.pi-check.pr-2
|
||||
| {{ _("Save Changes") }}
|
||||
|
||||
.form-group
|
||||
| {{ _("Change your full name, email, and password at") }} #[a(href="{{ blender_profile_url }}",target='_blank') Blender ID].
|
||||
.row.mt-3
|
||||
.col-md-9
|
||||
.form-group
|
||||
label {{ _("Full name") }}
|
||||
p {{ current_user.full_name }}
|
||||
.form-group
|
||||
label {{ _("E-mail") }}
|
||||
p {{ current_user.email }}
|
||||
.form-group
|
||||
| {{ _("Change your full name, email, avatar, and password at") }} #[a(href="{{ blender_profile_url }}",target='_blank') Blender ID].
|
||||
|
||||
| {% if current_user.badges_html %}
|
||||
.form-group
|
||||
p Your Blender ID badges:
|
||||
| {{ current_user.badges_html|safe }}
|
||||
p.hint-text Note that updates to these badges may take a few minutes to be visible here.
|
||||
| {% endif %}
|
||||
| {% if current_user.badges_html %}
|
||||
.form-group
|
||||
p Your Blender ID badges:
|
||||
| {{ current_user.badges_html|safe }}
|
||||
p.hint-text Note that updates to these badges may take a few minutes to be visible here.
|
||||
| {% endif %}
|
||||
|
||||
.py-3
|
||||
a(href="https://gravatar.com/")
|
||||
img.rounded-circle(src="{{ current_user.gravatar }}")
|
||||
span.p-3 {{ _("Change Gravatar") }}
|
||||
.col-md-3
|
||||
a(href="{{ blender_profile_url }}",target='_blank')
|
||||
current-user-avatar
|
||||
p
|
||||
small Your #[a(href="{{ blender_profile_url }}",target='_blank') Blender ID] avatar
|
||||
//- Avatar Sync button is commented out here, because it's not used by Blender Cloud.
|
||||
//- This tag, and the commented-out script tag below, are just examples.
|
||||
//- avatar-sync-button
|
||||
|
||||
.py-3
|
||||
button.btn.btn-outline-success.px-5.button-submit(type='submit')
|
||||
i.pi-check.pr-2
|
||||
| {{ _("Save Changes") }}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
| {{ super() }}
|
||||
//- script(src="{{ url_for('static_pillar', filename='assets/js/avatar.min.js') }}")
|
||||
script.
|
||||
new Vue({el:'#settings-form'});
|
||||
| {% endblock %}
|
||||
|
Reference in New Issue
Block a user