Merge pull request #5283 from tsiegleauq/user-active-toggle

Add set present toggle in user menu
This commit is contained in:
Emanuel Schütze 2020-03-30 10:14:27 +02:00 committed by GitHub
commit 8119507b8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 298 additions and 154 deletions

View File

@ -416,6 +416,13 @@ export class OperatorService implements OnAfterAppsLoaded {
this.operatorSubject.next(this.user); this.operatorSubject.next(this.user);
} }
/**
* Set the operators presence to isPresent
*/
public async setPresence(isPresent: boolean): Promise<void> {
await this.http.post(environment.urlPrefix + '/users/setpresence/', isPresent);
}
/** /**
* Returns a default WhoAmI response * Returns a default WhoAmI response
*/ */

View File

@ -0,0 +1,74 @@
<mat-expansion-panel class="user-menu mat-elevation-z0">
<mat-expansion-panel-header class="username">
<!-- Get the username from operator -->
{{ username }}
</mat-expansion-panel-header>
<mat-nav-list>
<!-- select languate -->
<a mat-list-item [matMenuTriggerFor]="languageMenu">
<mat-icon class="menu-icon">language</mat-icon>
<span class="menu-text">{{ getLangName() }}</span>
</a>
<div *ngIf="user && isLoggedIn">
<!-- present toggle -->
<button
[ngClass]="{ active: user.is_present }"
mat-menu-item
(click)="toggleUserIsPresent()"
*ngIf="allowSelfSetPresent"
>
<mat-icon [color]="user.is_present ? 'primary' : ''" class="menu-icon">
{{ user.is_present ? 'check_box' : 'check_box_outline_blank' }}
</mat-icon>
<span class="menu-text" translate>Present</span>
</button>
<!-- Show profile -->
<a
[ngClass]="{ active: isOnProfilePage() }"
[routerLink]="user ? ['/users/', user.id] : []"
(click)="onClickNavEntry()"
mat-list-item
>
<mat-icon class="menu-icon">person</mat-icon>
<span class="menu-text" translate>Show profile</span>
</a>
<!-- Change password -->
<ng-container *ngIf="authType === 'default'">
<a
[ngClass]="{ active: isOnChangePasswordPage() }"
*osPerms="'users.can_change_password'"
routerLink="/users/password"
(click)="onClickNavEntry()"
mat-list-item
>
<mat-icon class="menu-icon">vpn_key</mat-icon>
<span class="menu-text" translate>Change password</span>
</a>
</ng-container>
<ng-container *ngIf="authType === 'saml'">
<a *osPerms="'users.can_change_password'" [href]="samlChangePasswordUrl" mat-list-item>
<mat-icon class="menu-icon">vpn_key</mat-icon>
<span class="menu-text" translate>Change password</span>
</a>
</ng-container>
<!-- logout -->
<a (click)="logout()" mat-list-item>
<mat-icon class="menu-icon">exit_to_app</mat-icon>
<span class="menu-text" translate>Logout</span>
</a>
</div>
</mat-nav-list>
</mat-expansion-panel>
<mat-nav-list *ngIf="!isLoggedIn">
<a routerLink="/login" mat-list-item>
<mat-icon class="menu-icon">exit_to_app</mat-icon>
<span class="menu-text" translate>Login</span>
</a>
</mat-nav-list>
<mat-menu #languageMenu="matMenu">
<button mat-menu-item (click)="selectLang('en')">{{ getLangName('en') }}</button>
<button mat-menu-item (click)="selectLang('de')">{{ getLangName('de') }}</button>
<button mat-menu-item (click)="selectLang('ru')">{{ getLangName('ru') }}</button>
<button mat-menu-item (click)="selectLang('cs')">{{ getLangName('cs') }}</button>
</mat-menu>

View File

@ -0,0 +1,16 @@
.username {
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.menu-icon {
min-width: 20px !important; //puts the text to the right on the same level
margin-right: 25px !important;
}
.menu-text {
font-size: 16px;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { UserMenuComponent } from './user-menu.component';
describe('UserMenuComponent', () => {
let component: UserMenuComponent;
let fixture: ComponentFixture<UserMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UserMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,137 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from 'app/core/core-services/auth.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { LoginDataService } from 'app/core/ui-services/login-data.service';
import { OverlayService } from 'app/core/ui-services/overlay.service';
import { DEFAULT_AUTH_TYPE } from 'app/shared/models/users/user';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewUser } from 'app/site/users/models/view-user';
@Component({
selector: 'os-user-menu',
templateUrl: './user-menu.component.html',
styleUrls: ['./user-menu.component.scss']
})
export class UserMenuComponent extends BaseViewComponent implements OnInit {
public isLoggedIn: boolean;
public user: ViewUser;
public username = '';
public authType = DEFAULT_AUTH_TYPE;
public samlChangePasswordUrl: string | null = null;
public allowSelfSetPresent: boolean;
private selfPresentConfStr = 'users_allow_self_set_present';
@Output()
private navEvent: EventEmitter<void> = new EventEmitter();
public constructor(
titleService: Title,
protected translate: TranslateService,
protected matSnackBar: MatSnackBar,
private operator: OperatorService,
private authService: AuthService,
private overlayService: OverlayService, // private vp: ViewportService,
private loginDataService: LoginDataService,
private configService: ConfigService,
private router: Router
) {
super(titleService, translate, matSnackBar);
}
public ngOnInit(): void {
this.operator.getViewUserObservable().subscribe(user => {
if (user) {
this.user = user;
}
if (!this.operator.isAnonymous) {
this.username = user ? user.short_name : '';
this.isLoggedIn = true;
} else {
this.username = this.translate.instant('Guest');
this.isLoggedIn = false;
}
});
this.operator.authType.subscribe(authType => (this.authType = authType));
this.loginDataService.samlSettings.subscribe(
samlSettings => (this.samlChangePasswordUrl = samlSettings ? samlSettings.changePasswordUrl : null)
);
this.configService
.get<boolean>(this.selfPresentConfStr)
.subscribe(allowed => (this.allowSelfSetPresent = allowed));
}
public isOnProfilePage(): boolean {
const ownProfilePageUrl = `/users/${this.user.id}`;
return ownProfilePageUrl === this.router.url;
}
public isOnChangePasswordPage(): boolean {
const changePasswordPageUrl = '/users/password';
return changePasswordPageUrl === this.router.url;
}
/**
* Let the user change the language
* @param lang the desired language (en, de, cs, ...)
*/
public selectLang(selection: string): void {
this.translate.use(selection).subscribe();
}
/**
* Get the name of a Language by abbreviation.
*
* @param abbreviation The abbreviation of the languate or null, if the current
* language should be used.
*/
public getLangName(abbreviation?: string): string {
if (!abbreviation) {
abbreviation = this.translate.currentLang;
}
if (abbreviation === 'en') {
return 'English';
} else if (abbreviation === 'de') {
return 'Deutsch';
} else if (abbreviation === 'cs') {
return 'Čeština';
} else if (abbreviation === 'ru') {
return 'русский';
}
}
public toggleUserIsPresent(): void {
this.operator.setPresence(!this.user.is_present).catch(this.raiseError);
}
public onClickNavEntry(): void {
this.navEvent.next();
}
/**
* Function to log out the current user
*/
public logout(): void {
if (this.operator.guestsEnabled) {
this.overlayService.showSpinner(null, true);
}
this.authService.logout();
this.overlayService.logout();
}
}

View File

@ -107,7 +107,7 @@ import { SuperSearchComponent } from 'app/site/common/components/super-search/su
import { OverlayComponent } from 'app/site/common/components/overlay/overlay.component'; import { OverlayComponent } from 'app/site/common/components/overlay/overlay.component';
import { PreviewComponent } from './components/preview/preview.component'; import { PreviewComponent } from './components/preview/preview.component';
import { PdfViewerModule } from 'ng2-pdf-viewer'; import { PdfViewerModule } from 'ng2-pdf-viewer';
import { GlobalSpinnerComponent } from 'app/site/common/components/global-spinner/global-spinner.component';
import { HeightResizingDirective } from './directives/height-resizing.directive'; import { HeightResizingDirective } from './directives/height-resizing.directive';
import { TrustPipe } from './pipes/trust.pipe'; import { TrustPipe } from './pipes/trust.pipe';
import { LocalizedDatePipe } from './pipes/localized-date.pipe'; import { LocalizedDatePipe } from './pipes/localized-date.pipe';
@ -125,6 +125,9 @@ import { VotingPrivacyWarningComponent } from './components/voting-privacy-warni
import { MotionPollDetailContentComponent } from './components/motion-poll-detail-content/motion-poll-detail-content.component'; import { MotionPollDetailContentComponent } from './components/motion-poll-detail-content/motion-poll-detail-content.component';
import { AssignmentPollDetailContentComponent } from './components/assignment-poll-detail-content/assignment-poll-detail-content.component'; import { AssignmentPollDetailContentComponent } from './components/assignment-poll-detail-content/assignment-poll-detail-content.component';
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
import { UserMenuComponent } from './components/user-menu/user-menu.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
* *
@ -272,6 +275,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
ExtensionFieldComponent, ExtensionFieldComponent,
RoundedInputComponent, RoundedInputComponent,
GlobalSpinnerComponent, GlobalSpinnerComponent,
UserMenuComponent,
OverlayComponent, OverlayComponent,
PreviewComponent, PreviewComponent,
NgxMaterialTimepickerModule, NgxMaterialTimepickerModule,
@ -332,6 +336,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
RoundedInputComponent, RoundedInputComponent,
ProgressSnackBarComponent, ProgressSnackBarComponent,
GlobalSpinnerComponent, GlobalSpinnerComponent,
UserMenuComponent,
SuperSearchComponent, SuperSearchComponent,
OverlayComponent, OverlayComponent,
PreviewComponent, PreviewComponent,

View File

@ -15,83 +15,16 @@
> >
<div class="nav-toolbar"> <div class="nav-toolbar">
<!-- logo --> <!-- logo -->
<a routerLink="/" (click)="toggleSideNav()"> <a routerLink="/" (click)="mobileAutoCloseNav()">
<os-logo class="os-logo-container" [footer]="false"></os-logo> <os-logo class="os-logo-container" [footer]="false"></os-logo>
</a> </a>
</div> </div>
<!-- User Menu --> <!-- User Menu -->
<mat-expansion-panel class="user-menu mat-elevation-z0"> <os-user-menu (navEvent)="mobileAutoCloseNav()"></os-user-menu>
<mat-expansion-panel-header class="username">
<!-- Get the username from operator -->
{{ username }}
</mat-expansion-panel-header>
<mat-nav-list>
<a mat-list-item [matMenuTriggerFor]="languageMenu">
<mat-icon>language</mat-icon>
<span>{{ getLangName() }}</span>
</a>
<div *ngIf="isLoggedIn">
<a
[routerLink]="operator.user ? ['/users/', operator.user.id] : []"
(click)="mobileAutoCloseNav()"
mat-list-item
>
<mat-icon>person</mat-icon>
<span translate>Show profile</span>
</a>
<ng-container *ngIf="authType === 'default'">
<a
*osPerms="'users.can_change_password'"
routerLink="/users/password"
(click)="mobileAutoCloseNav()"
mat-list-item
>
<mat-icon>vpn_key</mat-icon>
<span translate>Change password</span>
</a>
</ng-container>
<ng-container *ngIf="authType === 'saml'">
<a
*osPerms="'users.can_change_password'"
[href]="samlChangePasswordUrl"
mat-list-item
>
<mat-icon>vpn_key</mat-icon>
<span translate>Change password</span>
</a>
</ng-container>
<a (click)="logout()" mat-list-item>
<mat-icon>exit_to_app</mat-icon>
<span translate>Logout</span>
</a>
</div>
<div *ngIf="!isLoggedIn">
<a routerLink="/login" mat-list-item>
<mat-icon>exit_to_app</mat-icon>
<span translate>Login</span>
</a>
</div>
</mat-nav-list>
</mat-expansion-panel>
<mat-menu #languageMenu="matMenu">
<button mat-menu-item (click)="selectLang('en')">{{ getLangName('en') }}</button>
<button mat-menu-item (click)="selectLang('de')">{{ getLangName('de') }}</button>
<button mat-menu-item (click)="selectLang('ru')">{{ getLangName('ru') }}</button>
<button mat-menu-item (click)="selectLang('cs')">{{ getLangName('cs') }}</button>
</mat-menu>
<!-- navigation --> <!-- navigation -->
<mat-nav-list class="main-nav"> <mat-nav-list class="main-nav">
<ng-container *ngIf="!isLoggedIn">
<a routerLink="/login" mat-list-item>
<mat-icon>exit_to_app</mat-icon>
<span translate>Login</span>
</a>
<mat-divider></mat-divider>
</ng-container>
<span *ngFor="let entry of mainMenuService.entries"> <span *ngFor="let entry of mainMenuService.entries">
<a <a
[@navItemAnim] [@navItemAnim]

View File

@ -39,14 +39,6 @@ mat-sidenav-container {
} }
} }
.username {
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.nav-toolbar { .nav-toolbar {
display: flex; display: flex;
margin: auto; margin: auto;

View File

@ -29,8 +29,7 @@
} }
/** style and align the nav icons the icons*/ /** style and align the nav icons the icons*/
.main-nav, .main-nav {
.user-menu {
mat-icon { mat-icon {
color: mat-color($foreground, icon); color: mat-color($foreground, icon);
} }

View File

@ -11,11 +11,8 @@ import { filter } from 'rxjs/operators';
import { navItemAnim } from '../shared/animations'; import { navItemAnim } from '../shared/animations';
import { OfflineService } from 'app/core/core-services/offline.service'; import { OfflineService } from 'app/core/core-services/offline.service';
import { LoginDataService } from 'app/core/ui-services/login-data.service';
import { OverlayService } from 'app/core/ui-services/overlay.service'; import { OverlayService } from 'app/core/ui-services/overlay.service';
import { UpdateService } from 'app/core/ui-services/update.service'; import { UpdateService } from 'app/core/ui-services/update.service';
import { DEFAULT_AUTH_TYPE } from 'app/shared/models/users/user';
import { AuthService } from '../core/core-services/auth.service';
import { BaseComponent } from '../base.component'; import { BaseComponent } from '../base.component';
import { MainMenuService } from '../core/core-services/main-menu.service'; import { MainMenuService } from '../core/core-services/main-menu.service';
import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service'; import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
@ -44,13 +41,6 @@ export class SiteComponent extends BaseComponent implements OnInit {
@ViewChild('sideNav', { static: true }) @ViewChild('sideNav', { static: true })
public sideNav: MatSidenav; public sideNav: MatSidenav;
/**
* Get the username from the operator (should be known already)
*/
public username = '';
public authType = DEFAULT_AUTH_TYPE;
/** /**
* is the user logged in, or the anonymous is active. * is the user logged in, or the anonymous is active.
*/ */
@ -76,12 +66,8 @@ export class SiteComponent extends BaseComponent implements OnInit {
*/ */
private delayedUpdateAvailable = false; private delayedUpdateAvailable = false;
public samlChangePasswordUrl: string | null = null;
/** /**
* Constructor * Constructor
*
* @param authService
* @param route * @param route
* @param operator * @param operator
* @param vp * @param vp
@ -96,7 +82,6 @@ export class SiteComponent extends BaseComponent implements OnInit {
protected translate: TranslateService, protected translate: TranslateService,
offlineService: OfflineService, offlineService: OfflineService,
private updateService: UpdateService, private updateService: UpdateService,
private authService: AuthService,
private router: Router, private router: Router,
public operator: OperatorService, public operator: OperatorService,
public vp: ViewportService, public vp: ViewportService,
@ -105,31 +90,15 @@ export class SiteComponent extends BaseComponent implements OnInit {
public OSStatus: OpenSlidesStatusService, public OSStatus: OpenSlidesStatusService,
public timeTravel: TimeTravelService, public timeTravel: TimeTravelService,
private matSnackBar: MatSnackBar, private matSnackBar: MatSnackBar,
private overlayService: OverlayService, private overlayService: OverlayService
private loginDataService: LoginDataService
) { ) {
super(title, translate); super(title, translate);
overlayService.showSpinner(translate.instant('Loading data. Please wait...')); overlayService.showSpinner(translate.instant('Loading data. Please wait...'));
this.operator.getViewUserObservable().subscribe(user => {
if (!operator.isAnonymous) {
this.username = user ? user.short_name : '';
this.isLoggedIn = true;
} else {
this.username = translate.instant('Guest');
this.isLoggedIn = false;
}
});
this.operator.authType.subscribe(authType => (this.authType = authType));
offlineService.isOffline().subscribe(offline => { offlineService.isOffline().subscribe(offline => {
this.isOffline = offline; this.isOffline = offline;
}); });
this.loginDataService.samlSettings.subscribe(
samlSettings => (this.samlChangePasswordUrl = samlSettings ? samlSettings.changePasswordUrl : null)
);
this.searchform = new FormGroup({ query: new FormControl([]) }); this.searchform = new FormGroup({ query: new FormControl([]) });
// detect routing data such as base perm and noInterruption // detect routing data such as base perm and noInterruption
@ -230,47 +199,6 @@ export class SiteComponent extends BaseComponent implements OnInit {
} }
} }
/**
* Let the user change the language
* @param lang the desired language (en, de, cs, ...)
*/
public selectLang(selection: string): void {
this.translate.use(selection).subscribe();
}
/**
* Get the name of a Language by abbreviation.
*
* @param abbreviation The abbreviation of the languate or null, if the current
* language should be used.
*/
public getLangName(abbreviation?: string): string {
if (!abbreviation) {
abbreviation = this.translate.currentLang;
}
if (abbreviation === 'en') {
return 'English';
} else if (abbreviation === 'de') {
return 'Deutsch';
} else if (abbreviation === 'cs') {
return 'Čeština';
} else if (abbreviation === 'ru') {
return 'русский';
}
}
/**
* Function to log out the current user
*/
public logout(): void {
if (this.operator.guestsEnabled) {
this.overlayService.showSpinner(null, true);
}
this.authService.logout();
this.overlayService.logout();
}
/** /**
* Handle swipes and gestures * Handle swipes and gestures
*/ */

View File

@ -15,7 +15,7 @@
@import './app/shared/components/projector-button/projector-button.component.scss'; @import './app/shared/components/projector-button/projector-button.component.scss';
@import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss'; @import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss';
@import './app/shared/components/sorting-tree/sorting-tree.component.scss'; @import './app/shared/components/sorting-tree/sorting-tree.component.scss';
@import './app/site/common/components/global-spinner/global-spinner.component.scss'; @import './app/shared/components/global-spinner/global-spinner.component.scss';
@import './app/shared/components/tile/tile.component.scss'; @import './app/shared/components/tile/tile.component.scss';
@import './app/shared/components/block-tile/block-tile.component.scss'; @import './app/shared/components/block-tile/block-tile.component.scss';
@import './app/shared/components/icon-container/icon-container.component.scss'; @import './app/shared/components/icon-container/icon-container.component.scss';

View File

@ -34,6 +34,15 @@ def get_config_variables():
group="Participants", group="Participants",
) )
yield ConfigVariable(
name="users_allow_self_set_present",
default_value=False,
input_type="boolean",
label="Allow users to set themselves as present",
weight=512,
group="Participants",
)
# PDF # PDF
yield ConfigVariable( yield ConfigVariable(

View File

@ -9,6 +9,7 @@ urlpatterns = [
url(r"^logout/$", views.UserLogoutView.as_view(), name="user_logout"), url(r"^logout/$", views.UserLogoutView.as_view(), name="user_logout"),
url(r"^whoami/$", views.WhoAmIView.as_view(), name="user_whoami"), url(r"^whoami/$", views.WhoAmIView.as_view(), name="user_whoami"),
url(r"^setpassword/$", views.SetPasswordView.as_view(), name="user_setpassword"), url(r"^setpassword/$", views.SetPasswordView.as_view(), name="user_setpassword"),
url(r"^setpresence/$", views.SetPresenceView.as_view(), name="user_setpresence"),
url( url(
r"^reset-password/$", r"^reset-password/$",
views.PasswordResetView.as_view(), views.PasswordResetView.as_view(),

View File

@ -719,6 +719,23 @@ class PersonalNoteViewSet(ModelViewSet):
# Special API views # Special API views
class SetPresenceView(APIView):
http_method_names = ["post"]
def post(self, request, *args, **kwargs):
user = request.user
if not config["users_allow_self_set_present"] or not user.is_authenticated:
raise ValidationError({"detail": "You cannot set your own presence"})
present = request.data
if present not in (True, False):
raise ValidationError({"detail": "Data must be a boolean"})
user.is_present = present
user.save()
return Response()
class WhoAmIDataView(APIView): class WhoAmIDataView(APIView):
def get_whoami_data(self): def get_whoami_data(self):
""" """