diff --git a/client/src/app/core/core-services/operator.service.ts b/client/src/app/core/core-services/operator.service.ts index a0d002a1c..699884f89 100644 --- a/client/src/app/core/core-services/operator.service.ts +++ b/client/src/app/core/core-services/operator.service.ts @@ -416,6 +416,13 @@ export class OperatorService implements OnAfterAppsLoaded { this.operatorSubject.next(this.user); } + /** + * Set the operators presence to isPresent + */ + public async setPresence(isPresent: boolean): Promise { + await this.http.post(environment.urlPrefix + '/users/setpresence/', isPresent); + } + /** * Returns a default WhoAmI response */ diff --git a/client/src/app/site/common/components/global-spinner/global-spinner.component.html b/client/src/app/shared/components/global-spinner/global-spinner.component.html similarity index 100% rename from client/src/app/site/common/components/global-spinner/global-spinner.component.html rename to client/src/app/shared/components/global-spinner/global-spinner.component.html diff --git a/client/src/app/site/common/components/global-spinner/global-spinner.component.scss b/client/src/app/shared/components/global-spinner/global-spinner.component.scss similarity index 100% rename from client/src/app/site/common/components/global-spinner/global-spinner.component.scss rename to client/src/app/shared/components/global-spinner/global-spinner.component.scss diff --git a/client/src/app/site/common/components/global-spinner/global-spinner.component.spec.ts b/client/src/app/shared/components/global-spinner/global-spinner.component.spec.ts similarity index 100% rename from client/src/app/site/common/components/global-spinner/global-spinner.component.spec.ts rename to client/src/app/shared/components/global-spinner/global-spinner.component.spec.ts diff --git a/client/src/app/site/common/components/global-spinner/global-spinner.component.ts b/client/src/app/shared/components/global-spinner/global-spinner.component.ts similarity index 100% rename from client/src/app/site/common/components/global-spinner/global-spinner.component.ts rename to client/src/app/shared/components/global-spinner/global-spinner.component.ts diff --git a/client/src/app/shared/components/user-menu/user-menu.component.html b/client/src/app/shared/components/user-menu/user-menu.component.html new file mode 100644 index 000000000..16da43d91 --- /dev/null +++ b/client/src/app/shared/components/user-menu/user-menu.component.html @@ -0,0 +1,74 @@ + + + + {{ username }} + + + + + language + {{ getLangName() }} + +
+ + + + + person + Show profile + + + + + vpn_key + Change password + + + + + vpn_key + Change password + + + + + exit_to_app + Logout + +
+
+
+ + + exit_to_app + Login + + + + + + + + + diff --git a/client/src/app/shared/components/user-menu/user-menu.component.scss b/client/src/app/shared/components/user-menu/user-menu.component.scss new file mode 100644 index 000000000..f12b390f1 --- /dev/null +++ b/client/src/app/shared/components/user-menu/user-menu.component.scss @@ -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; +} diff --git a/client/src/app/shared/components/user-menu/user-menu.component.spec.ts b/client/src/app/shared/components/user-menu/user-menu.component.spec.ts new file mode 100644 index 000000000..9d4e8c02f --- /dev/null +++ b/client/src/app/shared/components/user-menu/user-menu.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/components/user-menu/user-menu.component.ts b/client/src/app/shared/components/user-menu/user-menu.component.ts new file mode 100644 index 000000000..8a191626d --- /dev/null +++ b/client/src/app/shared/components/user-menu/user-menu.component.ts @@ -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 = 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(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(); + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 5410a0328..e871fe84d 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -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 { PreviewComponent } from './components/preview/preview.component'; 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 { TrustPipe } from './pipes/trust.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 { 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. * @@ -272,6 +275,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po ExtensionFieldComponent, RoundedInputComponent, GlobalSpinnerComponent, + UserMenuComponent, OverlayComponent, PreviewComponent, NgxMaterialTimepickerModule, @@ -332,6 +336,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po RoundedInputComponent, ProgressSnackBarComponent, GlobalSpinnerComponent, + UserMenuComponent, SuperSearchComponent, OverlayComponent, PreviewComponent, diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index 040e39857..197d187ef 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -15,83 +15,16 @@ > - - - - {{ username }} - - - - language - {{ getLangName() }} - - - - - - - - - - - + - - - exit_to_app - Login - - - - { - 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 => { this.isOffline = offline; }); - this.loginDataService.samlSettings.subscribe( - samlSettings => (this.samlChangePasswordUrl = samlSettings ? samlSettings.changePasswordUrl : null) - ); - this.searchform = new FormGroup({ query: new FormControl([]) }); // 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 */ diff --git a/client/src/styles.scss b/client/src/styles.scss index 5c2b2570b..cb4032953 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -15,7 +15,7 @@ @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/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/block-tile/block-tile.component.scss'; @import './app/shared/components/icon-container/icon-container.component.scss'; diff --git a/openslides/users/config_variables.py b/openslides/users/config_variables.py index 72c474f72..7b316bb85 100644 --- a/openslides/users/config_variables.py +++ b/openslides/users/config_variables.py @@ -34,6 +34,15 @@ def get_config_variables(): 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 yield ConfigVariable( diff --git a/openslides/users/urls.py b/openslides/users/urls.py index abe7c222c..3314a5cde 100644 --- a/openslides/users/urls.py +++ b/openslides/users/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ url(r"^logout/$", views.UserLogoutView.as_view(), name="user_logout"), url(r"^whoami/$", views.WhoAmIView.as_view(), name="user_whoami"), url(r"^setpassword/$", views.SetPasswordView.as_view(), name="user_setpassword"), + url(r"^setpresence/$", views.SetPresenceView.as_view(), name="user_setpresence"), url( r"^reset-password/$", views.PasswordResetView.as_view(), diff --git a/openslides/users/views.py b/openslides/users/views.py index fdb8ab0d2..9b09e92ad 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -719,6 +719,23 @@ class PersonalNoteViewSet(ModelViewSet): # 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): def get_whoami_data(self): """