Merge pull request #5347 from GabrielInTheWorld/statistics
Create statistics of closed list of speakers
This commit is contained in:
commit
7a31cff612
@ -62,6 +62,17 @@ const ListOfSpeakersNestedModelDescriptors: NestedModelDescriptors = {
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* An object, that contains information about structure-level,
|
||||
* speaking-time and finished-speakers.
|
||||
* Helpful to get a relation between speakers and their structure-level.
|
||||
*/
|
||||
export interface SpeakingTimeStructureLevelObject {
|
||||
structureLevel: string;
|
||||
finishedSpeakers: ViewSpeaker[];
|
||||
speakingTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository service for lists of speakers
|
||||
*
|
||||
@ -228,6 +239,95 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
||||
return !this.getViewModelList().some(list => list.hasSpeakerSpoken(speaker));
|
||||
}
|
||||
|
||||
/**
|
||||
* List every speaker only once, who has spoken
|
||||
*
|
||||
* @returns A list with all different speakers.
|
||||
*/
|
||||
public getAllFirstContributions(): ViewSpeaker[] {
|
||||
const speakers: ViewSpeaker[] = this.getViewModelList().flatMap(
|
||||
(los: ViewListOfSpeakers) => los.finishedSpeakers
|
||||
);
|
||||
const firstContributions: ViewSpeaker[] = [];
|
||||
for (const speaker of speakers) {
|
||||
if (!firstContributions.find(s => s.user_id === speaker.user_id)) {
|
||||
firstContributions.push(speaker);
|
||||
}
|
||||
}
|
||||
return firstContributions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps structure-level to speaker.
|
||||
*
|
||||
* @returns A list, which entries are `SpeakingTimeStructureLevelObject`.
|
||||
*/
|
||||
public getSpeakingTimeStructureLevelRelation(): SpeakingTimeStructureLevelObject[] {
|
||||
let listSpeakingTimeStructureLevel: SpeakingTimeStructureLevelObject[] = [];
|
||||
for (const los of this.getViewModelList()) {
|
||||
for (const speaker of los.finishedSpeakers) {
|
||||
const nextEntry = this.getSpeakingTimeStructureLevelObject(speaker);
|
||||
listSpeakingTimeStructureLevel = this.getSpeakingTimeStructureLevelList(
|
||||
nextEntry,
|
||||
listSpeakingTimeStructureLevel
|
||||
);
|
||||
}
|
||||
}
|
||||
return listSpeakingTimeStructureLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper-function to create a `SpeakingTimeStructureLevelObject` by a given speaker.
|
||||
*
|
||||
* @param speaker, with whom structure-level and speaking-time is calculated.
|
||||
*
|
||||
* @returns The created `SpeakingTimeStructureLevelObject`.
|
||||
*/
|
||||
private getSpeakingTimeStructureLevelObject(speaker: ViewSpeaker): SpeakingTimeStructureLevelObject {
|
||||
return {
|
||||
structureLevel:
|
||||
!speaker.user || (speaker.user && !speaker.user.structure_level)
|
||||
? 'No structure level'
|
||||
: speaker.user.structure_level,
|
||||
finishedSpeakers: [speaker],
|
||||
speakingTime: this.getSpeakingTimeAsNumber(speaker)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper-function to update entries in a given list, if already existing, or create entries otherwise.
|
||||
*
|
||||
* @param object A `SpeakingTimeStructureLevelObject`, that contains information about speaking-time
|
||||
* and structure-level.
|
||||
* @param list A list, at which speaking-time, structure-level and finished_speakers are set.
|
||||
*
|
||||
* @returns The updated map.
|
||||
*/
|
||||
private getSpeakingTimeStructureLevelList(
|
||||
object: SpeakingTimeStructureLevelObject,
|
||||
list: SpeakingTimeStructureLevelObject[]
|
||||
): SpeakingTimeStructureLevelObject[] {
|
||||
const index = list.findIndex(entry => entry.structureLevel === object.structureLevel);
|
||||
if (index >= 0) {
|
||||
list[index].speakingTime += object.speakingTime;
|
||||
list[index].finishedSpeakers.push(...object.finishedSpeakers);
|
||||
} else {
|
||||
list.push(object);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function calculates speaking-time as number for a given speaker.
|
||||
*
|
||||
* @param speaker The speaker, whose speaking-time should be calculated.
|
||||
*
|
||||
* @returns A number, that represents the speaking-time.
|
||||
*/
|
||||
private getSpeakingTimeAsNumber(speaker: ViewSpeaker): number {
|
||||
return Math.floor((new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function get the url to the speaker rest address
|
||||
*
|
||||
|
@ -62,6 +62,24 @@ export class DurationService {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a given time to a readable string, that contains hours, minutes and seconds.
|
||||
*
|
||||
* @param duration The time as number (in seconds).
|
||||
*
|
||||
* @returns A readable time-string.
|
||||
*/
|
||||
public durationToStringWithHours(duration: number): string {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = `0${Math.floor((duration % 3600) / 60)}`.slice(-2);
|
||||
const seconds = `0${Math.floor(duration % 60)}`.slice(-2);
|
||||
if (!isNaN(+minutes) && !isNaN(+seconds)) {
|
||||
return `${hours}:${minutes}:${seconds} h`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a duration number (given in minutes or seconds)
|
||||
*
|
||||
|
@ -484,7 +484,7 @@ export class ListViewTableComponent<V extends BaseViewModel | BaseViewModelWithC
|
||||
}
|
||||
|
||||
public isElementProjected = (context: PblNgridRowContext<V>) => {
|
||||
if (this.projectorService.isProjected(this.getProjectable(context.$implicit as V))) {
|
||||
if (this.allowProjector && this.projectorService.isProjected(this.getProjectable(context.$implicit as V))) {
|
||||
return 'projected';
|
||||
}
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
<mat-card class="os-card" *osPerms="'users.can_manage'">
|
||||
<button type="button" mat-button (click)="countUsers()" *ngIf="!this.token">
|
||||
<button type="button" mat-button (click)="countUsers()" *ngIf="!this.token">
|
||||
<span>{{ 'Count active users' | translate }}</span>
|
||||
</button>
|
||||
<button type="button" mat-button (click)="stopCounting()" *ngIf="this.token">
|
||||
</button>
|
||||
<button type="button" mat-button (click)="stopCounting()" *ngIf="this.token">
|
||||
<span>{{ 'Stop counting' | translate }}</span>
|
||||
</button>
|
||||
<div *ngIf="stats">
|
||||
</button>
|
||||
<div *ngIf="stats">
|
||||
<p>
|
||||
{{ userIds().length }} <span>{{ 'active users' | translate }}</span> ({{ stats.activeUserHandles }}
|
||||
<span>{{ 'connections' | translate }}</span
|
||||
@ -21,5 +20,4 @@
|
||||
>)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -34,4 +34,14 @@
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<os-count-users></os-count-users>
|
||||
<mat-card class="os-card" *osPerms="'users.can_manage'">
|
||||
<h3>{{ 'Statistics' | translate }}</h3>
|
||||
<div>
|
||||
<h4>{{ 'Count active users' | translate }}</h4>
|
||||
<os-count-users></os-count-users>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ 'Count active word requests' | translate }}</h4>
|
||||
<os-user-statistics></os-user-statistics>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
@ -0,0 +1,48 @@
|
||||
<button mat-button *ngIf="!openedStatistics" (click)="changeViewOfStatistics()">
|
||||
{{ 'Count active word requests' | translate }}
|
||||
</button>
|
||||
<button mat-button *ngIf="openedStatistics" (click)="changeViewOfStatistics()">
|
||||
{{ 'Stop counting' | translate }}
|
||||
</button>
|
||||
<ng-container *ngIf="openedStatistics">
|
||||
<table class="user-statistics-table">
|
||||
<tr>
|
||||
<td>{{ 'Total number of word requests' | translate }}</td>
|
||||
<td>{{ numberOfWordRequests }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'Unique speakers' | translate }}</td>
|
||||
<td>{{ numberOfUniqueSpeakers }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'Total duration of word requests' | translate }}</td>
|
||||
<td>{{ totalDuration }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<os-list-view-table
|
||||
[listObservable]="statisticsByStructureLevelObservable"
|
||||
[columns]="columnDefinition"
|
||||
[filterProps]="filterProps"
|
||||
[vScrollFixed]="45"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="false"
|
||||
[showListOfSpeakers]="false"
|
||||
[cssClasses]="{ 'user-statistics-table--virtual-scroll': true }"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'structureLevel'; row as object">
|
||||
{{ object.structureLevel | translate }}
|
||||
</div>
|
||||
<div *pblNgridCellDef="'durationOfWordRequests'; row as object">
|
||||
{{ parseDuration(object.speakingTime) }}
|
||||
</div>
|
||||
<div *pblNgridCellDef="'numberOfWordRequests'; row as object">
|
||||
{{ object.finishedSpeakers.length }}
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
</ng-container>
|
@ -0,0 +1,25 @@
|
||||
.user-statistics-table {
|
||||
width: 100%;
|
||||
margin: 16px auto;
|
||||
|
||||
&,
|
||||
td,
|
||||
th {
|
||||
border-collapse: collapse;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 8px 8px 8px 24px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.user-statistics-table--virtual-scroll {
|
||||
height: 500px;
|
||||
|
||||
.pbl-ngrid-header-cell:not(:first-child):before {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-user-statistics-style($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$text: mat-color($foreground, secondary-text);
|
||||
|
||||
.user-statistics-table {
|
||||
th {
|
||||
font-weight: 400;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&,
|
||||
td,
|
||||
th {
|
||||
border-color: mat-color($foreground, divider);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { UserStatisticsComponent } from './user-statistics.component';
|
||||
|
||||
describe('UserStatisticsComponent', () => {
|
||||
let component: UserStatisticsComponent;
|
||||
let fixture: ComponentFixture<UserStatisticsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserStatisticsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,149 @@
|
||||
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PblColumnDefinition } from '@pebula/ngrid/lib/grid';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
ListOfSpeakersRepositoryService,
|
||||
SpeakingTimeStructureLevelObject
|
||||
} from 'app/core/repositories/agenda/list-of-speakers-repository.service';
|
||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||
import { ViewSpeaker } from 'app/site/agenda/models/view-speaker';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
|
||||
@Component({
|
||||
selector: 'os-user-statistics',
|
||||
templateUrl: './user-statistics.component.html',
|
||||
styleUrls: ['./user-statistics.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class UserStatisticsComponent extends BaseViewComponent {
|
||||
/**
|
||||
* Returns the total duration for a whole assembly.
|
||||
*/
|
||||
public get totalDuration(): string {
|
||||
return this.parseDuration(this.speakingTimeAsNumber, true);
|
||||
}
|
||||
|
||||
public get numberOfUniqueSpeakers(): number {
|
||||
return this.uniqueSpeakers.length;
|
||||
}
|
||||
|
||||
public get numberOfWordRequests(): number {
|
||||
return this._numberOfWordRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a list. This list contains objects separated by the structure-level of speakers.
|
||||
*
|
||||
* Those objects hold information about number of word requests and duration of word requests for a given
|
||||
* structure-level.
|
||||
*/
|
||||
public get statisticsByStructureLevelObservable(): Observable<SpeakingTimeStructureLevelObject[]> {
|
||||
return this.relationSpeakingTimeStructureLevelSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated, if statistics are opened.
|
||||
*/
|
||||
public get openedStatistics(): boolean {
|
||||
return this.statisticIsOpen;
|
||||
}
|
||||
|
||||
public readonly columnDefinition: PblColumnDefinition[] = [
|
||||
{
|
||||
prop: 'structureLevel',
|
||||
width: 'auto',
|
||||
label: 'Structure level'
|
||||
},
|
||||
{
|
||||
prop: 'durationOfWordRequests',
|
||||
width: 'auto',
|
||||
label: 'Duration of word requests'
|
||||
},
|
||||
{
|
||||
prop: 'numberOfWordRequests',
|
||||
width: 'auto',
|
||||
label: 'Number of word requests'
|
||||
}
|
||||
];
|
||||
|
||||
public readonly filterProps: string[] = ['structureLevel'];
|
||||
|
||||
/**
|
||||
* Holds information about hours, minutes and seconds for the total duration of word requests.
|
||||
*/
|
||||
private speakingTimeAsNumber = 0;
|
||||
|
||||
/**
|
||||
* List of unique speakers.
|
||||
*/
|
||||
private uniqueSpeakers: ViewSpeaker[] = [];
|
||||
private _numberOfWordRequests = 0;
|
||||
private statisticIsOpen = false;
|
||||
private relationSpeakingTimeStructureLevelSubject = new BehaviorSubject<SpeakingTimeStructureLevelObject[]>([]);
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private losRepo: ListOfSpeakersRepositoryService,
|
||||
private durationService: DurationService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or closes statistics.
|
||||
*/
|
||||
public changeViewOfStatistics(): void {
|
||||
this.statisticIsOpen = !this.statisticIsOpen;
|
||||
if (this.statisticIsOpen) {
|
||||
this.startSubscription();
|
||||
} else {
|
||||
this.cleanSubjects();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This iterates over a list of list-of-speakers. For each speaker it calculates the duration the speaker
|
||||
* has spoken.
|
||||
*/
|
||||
private pushNextState(): void {
|
||||
const list = this.losRepo.getSpeakingTimeStructureLevelRelation();
|
||||
list.sort((a, b) => b.finishedSpeakers.length - a.finishedSpeakers.length);
|
||||
for (const entry of list) {
|
||||
this.speakingTimeAsNumber += entry.speakingTime;
|
||||
this._numberOfWordRequests += entry.finishedSpeakers.length;
|
||||
}
|
||||
this.relationSpeakingTimeStructureLevelSubject.next(list);
|
||||
this.uniqueSpeakers = this.losRepo.getAllFirstContributions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string from a given `TimeObject`.
|
||||
*/
|
||||
public parseDuration(time: number, withHours: boolean = false): string {
|
||||
return !withHours
|
||||
? this.durationService.durationToString(time, 'm')
|
||||
: this.durationService.durationToStringWithHours(time);
|
||||
}
|
||||
|
||||
private startSubscription(): void {
|
||||
this.subscriptions.push(
|
||||
this.losRepo.getViewModelListObservable().subscribe(() => {
|
||||
this.reset();
|
||||
this.pushNextState();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this._numberOfWordRequests = 0;
|
||||
this.speakingTimeAsNumber = 0;
|
||||
}
|
||||
}
|
@ -8,9 +8,17 @@ import { LegalNoticeComponent } from './components/legal-notice/legal-notice.com
|
||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { UserStatisticsComponent } from './components/user-statistics/user-statistics.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CommonRoutingModule, SharedModule],
|
||||
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, CountUsersComponent, ErrorComponent]
|
||||
declarations: [
|
||||
PrivacyPolicyComponent,
|
||||
StartComponent,
|
||||
LegalNoticeComponent,
|
||||
CountUsersComponent,
|
||||
ErrorComponent,
|
||||
UserStatisticsComponent
|
||||
]
|
||||
})
|
||||
export class OsCommonModule {}
|
||||
|
@ -34,6 +34,7 @@
|
||||
@import './app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
||||
@import './app/shared/components/jitsi/jitsi.component.scss-theme.scss';
|
||||
@import './app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
|
||||
@import './app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss';
|
||||
|
||||
/** fonts */
|
||||
@import './assets/styles/fonts.scss';
|
||||
@ -68,6 +69,7 @@ $narrow-spacing: (
|
||||
@include os-progress-snack-bar-style($theme);
|
||||
@include os-jitsi-theme($theme);
|
||||
@include os-list-view-table-theme($theme);
|
||||
@include os-user-statistics-style($theme);
|
||||
}
|
||||
|
||||
/** Load projector specific SCSS values */
|
||||
|
Loading…
Reference in New Issue
Block a user