Some overall improvements

Common:
	delete unused motion poll list

Poll Create form:
	Fix ugly multi line mat hints
	(workaround, see https://github.com/angular/components/issues/5227 )

Poll List:
	Fix too tiny column size
	user_has_voted_valid (ceck icon) was not shown

Motion Poll Card:
	Enhance subtitle layout (type + state)

Assignment Poll Card:
	Open warning after clicking the hint icon

Assignment Poll Chart:
	Show Absolute values and percents in chart label

Assignment Detail:
	Add new ballot button with plus icon instead of chart icon
This commit is contained in:
Sean 2020-03-16 18:40:31 +01:00 committed by FinnStutzenstein
parent 64f2720b1a
commit 3c9f6ed278
19 changed files with 168 additions and 183 deletions

View File

@ -21,26 +21,24 @@ import { PollData } from 'app/site/polls/services/poll.service';
name: 'pollPercentBase'
})
export class PollPercentBasePipe implements PipeTransform {
private decimalPlaces = 3;
public constructor(
private assignmentPollService: AssignmentPollService,
private motionPollService: MotionPollService
) {}
public transform(value: number, poll: PollData): string | null {
let totalByBase: number;
// logic handles over the pollService to avoid circular dependencies
let voteValueInPercent: string;
if ((<any>poll).assignment) {
totalByBase = this.assignmentPollService.getPercentBase(poll);
voteValueInPercent = this.assignmentPollService.getVoteValueInPercent(value, poll);
} else {
totalByBase = this.motionPollService.getPercentBase(poll);
voteValueInPercent = this.motionPollService.getVoteValueInPercent(value, poll);
}
if (totalByBase && totalByBase > 0) {
const percentNumber = (value / totalByBase) * 100;
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
return `(${result} %)`;
}
if (voteValueInPercent) {
return `(${voteValueInPercent})`;
} else {
return null;
}
}
}

View File

@ -75,7 +75,7 @@
<!-- New Ballot button -->
<div class="new-ballot-button" *ngIf="assignment && hasPerms('createPoll')">
<button mat-stroked-button (click)="openDialog()">
<mat-icon color="primary">poll</mat-icon>
<mat-icon>add</mat-icon>
<span translate>New ballot</span>
</button>
</div>

View File

@ -14,7 +14,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component';
import { VoteValue } from 'app/shared/models/poll/base-vote';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
import { PollService, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
import { PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
import { AssignmentPollService } from '../../services/assignment-poll.service';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
@ -25,7 +25,7 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
styleUrls: ['./assignment-poll-detail.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll, AssignmentPollService> {
public columnDefinitionSingleVotes: PblColumnDefinition[];
public filterProps = ['user.getFullName'];
@ -47,10 +47,9 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
groupRepo: GroupRepositoryService,
prompt: PromptService,
pollDialog: AssignmentPollDialogService,
pollService: PollService,
protected pollService: AssignmentPollService,
votesRepo: AssignmentVoteRepositoryService,
private operator: OperatorService,
private assignmentPollService: AssignmentPollService,
private router: Router
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
@ -144,11 +143,11 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
return true;
}
public getTableData(): PollTableData[] {
return this.pollService.generateTableData(this.poll);
}
protected onDeleted(): void {
this.router.navigate(['assignments', this.poll.assignment_id]);
}
public getTableData(): PollTableData[] {
return this.assignmentPollService.generateTableData(this.poll);
}
}

View File

@ -9,10 +9,12 @@ import { TranslateService } from '@ngx-translate/core';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component';
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollService } from 'app/site/polls/services/poll.service';
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
import { AssignmentPollService } from '../../services/assignment-poll.service';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
/**
@ -62,7 +64,7 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
promptService: PromptService,
repo: AssignmentPollRepositoryService,
pollDialog: AssignmentPollDialogService,
public pollService: PollService,
private pollService: AssignmentPollService,
private formBuilder: FormBuilder,
private pdfService: AssignmentPollPdfService
) {
@ -81,4 +83,8 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
public printBallot(): void {
this.pdfService.printBallots(this.poll);
}
public openVotingWarning(): void {
this.dialog.open(VotingPrivacyWarningComponent, infoDialogSettings);
}
}

View File

@ -11,6 +11,8 @@ import {
AssignmentPollPercentBase
} from 'app/shared/models/assignments/assignment-poll';
import { MajorityMethod, VOTE_UNDOCUMENTED } from 'app/shared/models/poll/base-poll';
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
import { PollData, PollService, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
@ -41,10 +43,12 @@ export class AssignmentPollService extends PollService {
public constructor(
config: ConfigService,
constants: ConstantsService,
private translate: TranslateService,
pollKeyVerbose: PollKeyVerbosePipe,
parsePollNumber: ParsePollNumberPipe,
protected translate: TranslateService,
private pollRepo: AssignmentPollRepositoryService
) {
super(constants);
super(constants, translate, pollKeyVerbose, parsePollNumber);
config
.get<AssignmentPollPercentBase>('assignment_poll_default_100_percent_base')
.subscribe(base => (this.defaultPercentBase = base));

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
@ -14,8 +14,8 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
import { PollService } from 'app/site/polls/services/poll.service';
@Component({
selector: 'os-motion-poll-detail',
@ -23,7 +23,7 @@ import { PollService } from 'app/site/polls/services/poll.service';
styleUrls: ['./motion-poll-detail.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll, MotionPollService> {
public motion: ViewMotion;
public columnDefinition: PblColumnDefinition[] = [
{
@ -49,7 +49,7 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
groupRepo: GroupRepositoryService,
prompt: PromptService,
pollDialog: MotionPollDialogService,
pollService: PollService,
pollService: MotionPollService,
votesRepo: MotionVoteRepositoryService,
private operator: OperatorService,
private router: Router

View File

@ -1,36 +0,0 @@
<os-head-bar>
<div class="title-slot">Motions poll list</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="pollMenu"><mat-icon>more_vert</mat-icon></button>
</div>
</os-head-bar>
<os-list-view-table
[listObservableProvider]="repo"
[vScrollFixed]="64"
[columns]="tableColumnDefinition"
[listStorageKey]="'motion-polls'"
>
<div *pblNgridCellDef="'title'; row as poll; rowContext as context" class="cell-slot fill">
<a
class="detail-link"
(click)="saveScrollIndex('motion-polls', rowContext.identity)"
[routerLink]="poll.id"
*ngIf="!isMultiSelect"
></a>
<span>{{ poll.title }}</span>
</div>
<div *pblNgridCellDef="'state'; row as poll; rowContext as context" class="cell-slot fill">
<span>{{ poll.stateVerbose }}</span>
</div>
</os-list-view-table>
<mat-menu #pollMenu="matMenu">
<!-- Settings -->
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/polls">
<mat-icon>settings</mat-icon>
<span translate>Settings</span>
</button>
</mat-menu>

View File

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

View File

@ -1,45 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { StorageService } from 'app/core/core-services/storage.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { BaseListViewComponent } from 'app/site/base/base-list-view';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
@Component({
selector: 'os-motion-poll-list',
templateUrl: './motion-poll-list.component.html',
styleUrls: ['./motion-poll-list.component.scss']
})
export class MotionPollListComponent extends BaseListViewComponent<ViewMotionPoll> implements OnInit {
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'title',
width: 'auto'
},
{
prop: 'state',
width: 'auto'
}
];
public polls: ViewMotionPoll[] = [];
public constructor(
title: Title,
protected translate: TranslateService,
matSnackbar: MatSnackBar,
storage: StorageService,
public repo: MotionPollRepositoryService
) {
super(title, translate, matSnackbar, storage);
}
public ngOnInit(): void {
this.subscriptions.push(this.repo.getViewModelListObservable().subscribe(polls => (this.polls = polls)));
}
}

View File

@ -2,10 +2,8 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MotionPollDetailComponent } from './motion-poll-detail/motion-poll-detail.component';
import { MotionPollListComponent } from './motion-poll-list/motion-poll-list.component';
const routes: Routes = [
{ path: '', component: MotionPollListComponent, pathMatch: 'full' },
{ path: 'new', component: MotionPollDetailComponent },
{ path: ':id', component: MotionPollDetailComponent }
];

View File

@ -4,7 +4,6 @@ import { NgModule } from '@angular/core';
import { SharedModule } from 'app/shared/shared.module';
import { PollsModule } from 'app/site/polls/polls.module';
import { MotionPollDetailComponent } from './motion-poll-detail/motion-poll-detail.component';
import { MotionPollListComponent } from './motion-poll-list/motion-poll-list.component';
import { MotionPollRoutingModule } from './motion-poll-routing.module';
import { MotionPollVoteComponent } from './motion-poll-vote/motion-poll-vote.component';
import { MotionPollComponent } from './motion-poll/motion-poll.component';
@ -12,6 +11,6 @@ import { MotionPollComponent } from './motion-poll/motion-poll.component';
@NgModule({
imports: [CommonModule, SharedModule, MotionPollRoutingModule, PollsModule],
exports: [MotionPollComponent],
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollListComponent, MotionPollVoteComponent]
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollVoteComponent]
})
export class MotionPollModule {}

View File

@ -2,15 +2,25 @@
<!-- Poll Infos -->
<div class="poll-title-wrapper">
<!-- Title Area -->
<div class="poll-title-area spacer-bottom-20">
<div class="poll-title-area">
<!-- Title -->
<span class="poll-title">
<a [routerLink]="pollLink">
{{ poll.title | translate }}
</a>
</span>
</div>
<div class="italic">
<!-- Dot Menu -->
<span class="poll-actions" *osPerms="'motions.can_manage_polls'">
<button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
<mat-icon class="small-icon">more_horiz</mat-icon>
</button>
</span>
</div>
<!-- Subtitle -->
<div class="italic spacer-bottom-20">
<span *osPerms="'motions.can_manage_polls'; and: poll.type === 'pseudoanonymous'">
<button mat-icon-button color="warn" (click)="openVotingWarning()">
<mat-icon>
@ -29,15 +39,6 @@
{{ poll.stateVerbose | translate }}
</span>
</div>
</div>
<!-- Dot Menu -->
<span class="poll-actions" *osPerms="'motions.can_manage_polls'">
<button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
<mat-icon class="small-icon">more_horiz</mat-icon>
</button>
</span>
</div>
<!-- Change state button -->
<div *osPerms="'motions.can_manage_polls'; and: !hideChangeState">

View File

@ -7,6 +7,8 @@ import { MotionPollRepositoryService } from 'app/core/repositories/motions/motio
import { ConfigService } from 'app/core/ui-services/config.service';
import { MotionPoll, MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
import { PollData, PollService, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
import { ViewMotionOption } from '../models/view-motion-option';
import { ViewMotionPoll } from '../models/view-motion-poll';
@ -43,10 +45,12 @@ export class MotionPollService extends PollService {
public constructor(
config: ConfigService,
constants: ConstantsService,
private translate: TranslateService,
pollKeyVerbose: PollKeyVerbosePipe,
parsePollNumber: ParsePollNumberPipe,
protected translate: TranslateService,
private pollRepo: MotionPollRepositoryService
) {
super(constants);
super(constants, translate, pollKeyVerbose, parsePollNumber);
config
.get<PercentBase>('motion_poll_default_100_percent_base')
.subscribe(base => (this.defaultPercentBase = base));

View File

@ -27,7 +27,8 @@ export interface BaseVoteData {
user?: ViewUser;
}
export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends BaseViewComponent implements OnInit {
export abstract class BasePollDetailComponent<V extends ViewBasePoll, S extends PollService> extends BaseViewComponent
implements OnInit {
/**
* All the groups of users.
*/
@ -100,7 +101,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
protected groupRepo: GroupRepositoryService,
protected promptService: PromptService,
protected pollDialog: BasePollDialogService<V>,
protected pollService: PollService,
protected pollService: S,
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>
) {
super(title, translate, matSnackbar);

View File

@ -10,20 +10,35 @@
[filterProps]="filterProps"
[filterService]="filterService"
>
<div *pblNgridCellDef="'title'; row as poll; rowContext as context;" class="cell-slot fill">
<!-- Poll Title -->
<div *pblNgridCellDef="'title'; row as poll; rowContext as context" class="cell-slot fill">
<a class="detail-link" [routerLink]="poll.parentLink" *ngIf="!isMultiSelect"></a>
<span>{{ poll.title }}</span>
</div>
<div *pblNgridCellDef="'classType'; row as poll;" class="cell-slot fill">
<!-- Motion Or Assigmnent Title Title -->
<div *pblNgridCellDef="'classType'; row as poll" class="cell-slot fill">
<a class="detail-link" [routerLink]="poll.parentLink" *ngIf="!isMultiSelect"></a>
<span>{{ poll.getContentObject().getListTitle() }}</span>
</div>
<div *pblNgridCellDef="'state'; row as poll;" class="cell-slot fill">
<!-- State -->
<div *pblNgridCellDef="'state'; row as poll" class="cell-slot fill">
<a class="detail-link" [routerLink]="poll.parentLink" *ngIf="!isMultiSelect"></a>
<span>{{ poll.stateVerbose | translate }}</span>
</div>
<div *pblNgridCellDef="'votability'; row as poll;" class="cell-slot fill">
<mat-icon *ngIf="poll.user_has_voted_valid" color="accent" matTooltip="{{ 'You have already voted.' | translate }}">check_circle</mat-icon>
<mat-icon *ngIf="!poll.user_has_voted_valid && poll.canBeVotedFor" color="warn" matTooltip="{{ 'Voting is currently in progress.' | translate }}">warning</mat-icon>
<!-- Voted Indicator -->
<div *pblNgridCellDef="'votability'; row as poll" class="cell-slot fill">
<mat-icon *ngIf="poll.user_has_voted" color="accent" matTooltip="{{ 'You have already voted.' | translate }}">
check_circle
</mat-icon>
<mat-icon
*ngIf="!poll.user_has_voted && poll.canBeVotedFor"
color="warn"
matTooltip="{{ 'Voting is currently in progress.' | translate }}"
>
warning
</mat-icon>
</div>
</os-list-view-table>

View File

@ -29,7 +29,7 @@ export class PollListComponent extends BaseListViewComponent<ViewBasePoll> {
},
{
prop: 'state',
width: '70px'
width: 'auto'
},
{
prop: 'votability',

View File

@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { _ } from 'app/core/translate/translation-marker';
import { ChartData, ChartDate } from 'app/shared/components/charts/charts.component';
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
@ -11,6 +13,8 @@ import {
PollType,
VOTE_UNDOCUMENTED
} from 'app/shared/models/poll/base-poll';
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
import { AssignmentPollMethodVerbose } from 'app/site/assignments/models/view-assignment-poll';
import {
MajorityMethodVerbose,
@ -21,6 +25,7 @@ import {
} from 'app/site/polls/models/view-base-poll';
import { ConstantsService } from '../../../core/core-services/constants.service';
const PERCENT_DECIMAL_PLACES = 3;
/**
* The possible keys of a poll object that represent numbers.
* TODO Should be 'key of MotionPoll|AssinmentPoll if type of key is number'
@ -178,12 +183,32 @@ export abstract class PollService {
*/
public pollValues: CalculablePollKey[] = ['yes', 'no', 'abstain', 'votesvalid', 'votesinvalid', 'votescast'];
public constructor(constants: ConstantsService) {
public constructor(
constants: ConstantsService,
protected translate: TranslateService,
private pollKeyVerbose: PollKeyVerbosePipe,
private parsePollNumber: ParsePollNumberPipe
) {
constants
.get<OpenSlidesSettings>('Settings')
.subscribe(settings => (this.isElectronicVotingEnabled = settings.ENABLE_ELECTRONIC_VOTING));
}
/**
* return the total number of votes depending on the selected percent base
*/
public abstract getPercentBase(poll: PollData): number;
public getVoteValueInPercent(value: number, poll: PollData): string | null {
const totalByBase = this.getPercentBase(poll);
if (totalByBase && totalByBase > 0) {
const percentNumber = (value / totalByBase) * 100;
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(PERCENT_DECIMAL_PLACES);
return `${result} %`;
}
return null;
}
/**
* Assigns the default poll data to the object. To be extended in subclasses
* @param poll the poll/object to fill
@ -268,8 +293,24 @@ export abstract class PollService {
}
public generateChartData(poll: PollData | ViewBasePoll): ChartData {
const fields = this.getPollDataFields(poll);
const data: ChartData = fields.map(key => {
return {
data: this.getResultFromPoll(poll, key),
label: key.toUpperCase(),
backgroundColor: PollColor[key],
hoverBackgroundColor: PollColor[key]
} as ChartDate;
});
return data;
}
private getPollDataFields(poll: PollData | ViewBasePoll): CalculablePollKey[] {
let fields: CalculablePollKey[];
let isAssignment: boolean;
if (poll instanceof ViewBasePoll) {
isAssignment = poll.pollClassType === 'assignment';
} else {
@ -294,16 +335,7 @@ export abstract class PollService {
}
}
const data: ChartData = fields.map(key => {
return {
data: this.getResultFromPoll(poll, key),
label: key.toUpperCase(),
backgroundColor: PollColor[key],
hoverBackgroundColor: PollColor[key]
} as ChartDate;
});
return data;
return fields;
}
/**
@ -314,7 +346,17 @@ export abstract class PollService {
}
public getChartLabels(poll: PollData): string[] {
return poll.options.map(candidate => candidate.user.short_name);
const fields = this.getPollDataFields(poll);
return poll.options.map(option => {
const votingResults = fields.map(field => {
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(field));
const resultValue = this.parsePollNumber.transform(option[field]);
const resultInPercent = this.getVoteValueInPercent(option[field], poll);
return `${votingKey} ${resultValue} (${resultInPercent})`;
});
return `${option.user.short_name} · ${votingResults.join(' · ')}`;
});
}
public isVoteDocumented(vote: number): boolean {

View File

@ -864,6 +864,32 @@ button.mat-menu-item.selected {
}
}
/**
* Fix to enable multi line mat hints. See:
* https://github.com/angular/components/issues/5227
*/
.mat-form-field {
.mat-form-field-wrapper {
padding-bottom: 0;
.mat-form-field-underline {
position: initial !important;
display: block;
margin-top: -1px;
}
.mat-form-field-subscript-wrapper,
.mat-form-field-ripple {
position: initial !important;
display: table;
}
.mat-form-field-subscript-wrapper {
min-height: calc(1em + 1px);
}
}
}
/**
* Use to disable events on (i.e) matMenuTriggerFor
*/