Initial work for supporting voting
This commit is contained in:
parent
4d4697eee0
commit
ced40cab74
@ -85,7 +85,11 @@ matrix:
|
|||||||
- "3.6"
|
- "3.6"
|
||||||
script:
|
script:
|
||||||
- mypy openslides/ tests/
|
- mypy openslides/ tests/
|
||||||
|
<<<<<<< HEAD
|
||||||
- pytest --cov --cov-fail-under=72
|
- pytest --cov --cov-fail-under=72
|
||||||
|
=======
|
||||||
|
- pytest --cov --cov-fail-under=74
|
||||||
|
>>>>>>> Initial work for supporting voting
|
||||||
|
|
||||||
- name: "Server: Tests Python 3.7"
|
- name: "Server: Tests Python 3.7"
|
||||||
language: python
|
language: python
|
||||||
@ -96,7 +100,11 @@ matrix:
|
|||||||
- isort --check-only --diff --recursive openslides tests
|
- isort --check-only --diff --recursive openslides tests
|
||||||
- black --check --diff --target-version py36 openslides tests
|
- black --check --diff --target-version py36 openslides tests
|
||||||
- mypy openslides/ tests/
|
- mypy openslides/ tests/
|
||||||
|
<<<<<<< HEAD
|
||||||
- pytest --cov --cov-fail-under=72
|
- pytest --cov --cov-fail-under=72
|
||||||
|
=======
|
||||||
|
- pytest --cov --cov-fail-under=74
|
||||||
|
>>>>>>> Initial work for supporting voting
|
||||||
|
|
||||||
- name: "Server: Tests Python 3.8"
|
- name: "Server: Tests Python 3.8"
|
||||||
language: python
|
language: python
|
||||||
|
@ -68,15 +68,10 @@ export class AppLoadService {
|
|||||||
let repository: BaseRepository<any, any, any> = null;
|
let repository: BaseRepository<any, any, any> = null;
|
||||||
repository = this.injector.get(entry.repository);
|
repository = this.injector.get(entry.repository);
|
||||||
repositories.push(repository);
|
repositories.push(repository);
|
||||||
this.modelMapper.registerCollectionElement(
|
this.modelMapper.registerCollectionElement(entry.model, entry.viewModel, repository);
|
||||||
entry.collectionString,
|
|
||||||
entry.model,
|
|
||||||
entry.viewModel,
|
|
||||||
repository
|
|
||||||
);
|
|
||||||
if (this.isSearchableModelEntry(entry)) {
|
if (this.isSearchableModelEntry(entry)) {
|
||||||
this.searchService.registerModel(
|
this.searchService.registerModel(
|
||||||
entry.collectionString,
|
entry.model.COLLECTIONSTRING,
|
||||||
repository,
|
repository,
|
||||||
entry.searchOrder,
|
entry.searchOrder,
|
||||||
entry.openInNewTab
|
entry.openInNewTab
|
||||||
@ -108,7 +103,7 @@ export class AppLoadService {
|
|||||||
// to check if the result of the contructor (the model instance) is really a searchable.
|
// to check if the result of the contructor (the model instance) is really a searchable.
|
||||||
if (!isSearchable(new entry.viewModel())) {
|
if (!isSearchable(new entry.viewModel())) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Wrong configuration for ${entry.collectionString}: you gave a searchOrder, but the model is not searchable.`
|
`Wrong configuration for ${entry.model.COLLECTIONSTRING}: you gave a searchOrder, but the model is not searchable.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -47,12 +47,11 @@ export class CollectionStringMapperService {
|
|||||||
* @param model
|
* @param model
|
||||||
*/
|
*/
|
||||||
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
||||||
collectionString: string,
|
|
||||||
model: ModelConstructor<M>,
|
model: ModelConstructor<M>,
|
||||||
viewModel: ViewModelConstructor<V>,
|
viewModel: ViewModelConstructor<V>,
|
||||||
repository: BaseRepository<V, M, TitleInformation>
|
repository: BaseRepository<V, M, TitleInformation>
|
||||||
): void {
|
): void {
|
||||||
this.collectionStringMapping[collectionString] = [model, viewModel, repository];
|
this.collectionStringMapping[model.COLLECTIONSTRING] = [model, viewModel, repository];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +7,6 @@ import { MainMenuEntry } from '../core-services/main-menu.service';
|
|||||||
import { Searchable } from '../../site/base/searchable';
|
import { Searchable } from '../../site/base/searchable';
|
||||||
|
|
||||||
interface BaseModelEntry {
|
interface BaseModelEntry {
|
||||||
collectionString: string;
|
|
||||||
repository: Type<BaseRepository<any, any, any>>;
|
repository: Type<BaseRepository<any, any, any>>;
|
||||||
model: ModelConstructor<BaseModel>;
|
model: ModelConstructor<BaseModel>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollRepositoryService } from './assignment-poll-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentPollRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentPollRepositoryService = TestBed.get(AssignmentPollRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentPollRepositoryService extends BaseRepository<
|
||||||
|
ViewAssignmentPoll,
|
||||||
|
AssignmentPoll,
|
||||||
|
AssignmentPollTitleInformation
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Constructor for the Assignment Repository.
|
||||||
|
*
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentPoll
|
||||||
|
// TODO: relations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: AssignmentPollTitleInformation) => {
|
||||||
|
return titleInformation.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
|
};
|
||||||
|
}
|
@ -8,12 +8,12 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
|
|||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
|
||||||
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
||||||
import { AssignmentTitleInformation, ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
import { AssignmentTitleInformation, ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option';
|
|
||||||
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
@ -68,8 +68,8 @@ const AssignmentNestedModelDescriptors: NestedModelDescriptors = {
|
|||||||
'assignments/assignment-poll': [
|
'assignments/assignment-poll': [
|
||||||
{
|
{
|
||||||
ownKey: 'options',
|
ownKey: 'options',
|
||||||
foreignViewModel: ViewAssignmentPollOption,
|
foreignViewModel: ViewAssignmentOption,
|
||||||
foreignModel: AssignmentPollOption,
|
foreignModel: AssignmentOption,
|
||||||
order: 'weight',
|
order: 'weight',
|
||||||
relationDefinitionsByKey: {
|
relationDefinitionsByKey: {
|
||||||
user: {
|
user: {
|
||||||
@ -97,10 +97,8 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
AssignmentTitleInformation
|
AssignmentTitleInformation
|
||||||
> {
|
> {
|
||||||
private readonly restPath = '/rest/assignments/assignment/';
|
private readonly restPath = '/rest/assignments/assignment/';
|
||||||
private readonly restPollPath = '/rest/assignments/poll/';
|
|
||||||
private readonly candidatureOtherPath = '/candidature_other/';
|
private readonly candidatureOtherPath = '/candidature_other/';
|
||||||
private readonly candidatureSelfPath = '/candidature_self/';
|
private readonly candidatureSelfPath = '/candidature_self/';
|
||||||
private readonly createPollPath = '/create_poll/';
|
|
||||||
private readonly markElectedPath = '/mark_elected/';
|
private readonly markElectedPath = '/mark_elected/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,67 +177,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Poll to a given assignment
|
|
||||||
*
|
|
||||||
* @param assignment The assignment to add the poll to
|
|
||||||
*/
|
|
||||||
public async addPoll(assignment: ViewAssignment): Promise<void> {
|
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
|
|
||||||
// TODO: change current tab to new poll
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a poll
|
|
||||||
*
|
|
||||||
* @param id id of the poll to delete
|
|
||||||
*/
|
|
||||||
public async deletePoll(poll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
await this.httpService.delete(`${this.restPollPath}${poll.id}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update data (metadata etc) for a poll
|
|
||||||
*
|
|
||||||
* @param poll the (partial) data to update
|
|
||||||
* @param originalPoll the poll to update
|
|
||||||
*
|
|
||||||
* TODO: check if votes is untouched
|
|
||||||
*/
|
|
||||||
public async updatePoll(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
const data: AssignmentPoll = Object.assign(originalPoll.poll, poll);
|
|
||||||
await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: temporary (?) update votes method. Needed because server needs
|
|
||||||
* different input than it's output in case of votes ?
|
|
||||||
*
|
|
||||||
* @param poll the updated Poll
|
|
||||||
* @param originalPoll the original poll
|
|
||||||
*/
|
|
||||||
public async updateVotes(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
const votes = poll.options.map(option => {
|
|
||||||
const voteObject = {};
|
|
||||||
for (const vote of option.votes) {
|
|
||||||
voteObject[vote.value] = vote.weight;
|
|
||||||
}
|
|
||||||
return voteObject;
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
assignment_id: originalPoll.assignment_id,
|
|
||||||
votes: votes,
|
|
||||||
votesabstain: poll.votesabstain || null,
|
|
||||||
votescast: poll.votescast || null,
|
|
||||||
votesinvalid: poll.votesinvalid || null,
|
|
||||||
votesno: poll.votesno || null,
|
|
||||||
votesvalid: poll.votesvalid || null
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.httpService.put(`${this.restPollPath}${originalPoll.id}/`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* change the 'elected' state of an election candidate
|
* change the 'elected' state of an election candidate
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentVoteRepositoryService } from './assignment-vote-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentVoteRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentVoteRepositoryService = TestBed.get(AssignmentVoteRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
|
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentVoteRepositoryService extends BaseRepository<ViewAssignmentVote, AssignmentVote, object> {
|
||||||
|
/**
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentVote
|
||||||
|
// TODO: relations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Vote';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionPollRepositoryService } from './motion-poll-repository.service';
|
||||||
|
|
||||||
|
describe('MotionPollRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionPollRepositoryService = TestBed.get(MotionPollRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
|
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionPollRepositoryService extends BaseRepository<
|
||||||
|
ViewMotionPoll,
|
||||||
|
MotionPoll,
|
||||||
|
MotionPollTitleInformation
|
||||||
|
> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionPoll
|
||||||
|
// TODO: relations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: MotionPollTitleInformation) => {
|
||||||
|
return titleInformation.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
|
};
|
||||||
|
}
|
@ -14,7 +14,6 @@ import { ConfigService } from 'app/core/ui-services/config.service';
|
|||||||
import { DiffLinesInParagraph, DiffService } from 'app/core/ui-services/diff.service';
|
import { DiffLinesInParagraph, DiffService } from 'app/core/ui-services/diff.service';
|
||||||
import { TreeIdNode } from 'app/core/ui-services/tree.service';
|
import { TreeIdNode } from 'app/core/ui-services/tree.service';
|
||||||
import { Motion } from 'app/shared/models/motions/motion';
|
import { Motion } from 'app/shared/models/motions/motion';
|
||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
|
||||||
import { Submitter } from 'app/shared/models/motions/submitter';
|
import { Submitter } from 'app/shared/models/motions/submitter';
|
||||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
||||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||||
@ -844,46 +843,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
.filter((para: ViewMotionAmendedParagraph) => para !== null);
|
.filter((para: ViewMotionAmendedParagraph) => para !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a request to the server, creating a new poll for the motion
|
|
||||||
*/
|
|
||||||
public async createPoll(motion: ViewMotion): Promise<void> {
|
|
||||||
const url = '/rest/motions/motion/' + motion.id + '/create_poll/';
|
|
||||||
await this.httpService.post(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an update request for a poll.
|
|
||||||
*
|
|
||||||
* @param poll
|
|
||||||
*/
|
|
||||||
public async updatePoll(poll: MotionPoll): Promise<void> {
|
|
||||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
|
||||||
const data = {
|
|
||||||
motion_id: poll.motion_id,
|
|
||||||
id: poll.id,
|
|
||||||
votescast: poll.votescast,
|
|
||||||
votesvalid: poll.votesvalid,
|
|
||||||
votesinvalid: poll.votesinvalid,
|
|
||||||
votes: {
|
|
||||||
Yes: poll.yes,
|
|
||||||
No: poll.no,
|
|
||||||
Abstain: poll.abstain
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await this.httpService.put(url, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a http request to delete the given poll
|
|
||||||
*
|
|
||||||
* @param poll
|
|
||||||
*/
|
|
||||||
public async deletePoll(poll: MotionPoll): Promise<void> {
|
|
||||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
|
||||||
await this.httpService.delete(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals the acceptance of the current recommendation to the server
|
* Signals the acceptance of the current recommendation to the server
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionVoteRepositoryService } from './motion-vote-repository.service';
|
||||||
|
|
||||||
|
describe('MotionVoteRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionVoteRepositoryService = TestBed.get(MotionVoteRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
||||||
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionVoteRepositoryService extends BaseRepository<ViewMotionVote, MotionVote, object> {
|
||||||
|
/**
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionVote
|
||||||
|
// TODO: relations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Vote';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { BaseOption } from '../poll/base-option';
|
||||||
|
|
||||||
|
export class AssignmentOption extends BaseOption<AssignmentOption> {
|
||||||
|
public static COLLECTIONSTRING = 'assignments/assignment-option';
|
||||||
|
|
||||||
|
public user_id: number;
|
||||||
|
|
||||||
|
public constructor(input?: any) {
|
||||||
|
super(AssignmentOption.COLLECTIONSTRING, input);
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
|
||||||
import { BaseModel } from '../base/base-model';
|
|
||||||
|
|
||||||
export interface AssignmentOptionVote {
|
|
||||||
weight: number;
|
|
||||||
value: PollVoteValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Representation of a poll option
|
|
||||||
*
|
|
||||||
* part of the 'polls-options'-array in poll
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
export class AssignmentPollOption extends BaseModel<AssignmentPollOption> {
|
|
||||||
public static COLLECTIONSTRING = 'assignments/assignment-poll-option';
|
|
||||||
|
|
||||||
public id: number; // The AssignmentPollOption id
|
|
||||||
public candidate_id: number; // the user id of the candidate
|
|
||||||
public is_elected: boolean;
|
|
||||||
public votes: AssignmentOptionVote[];
|
|
||||||
public poll_id: number;
|
|
||||||
public weight: number; // weight to order the display
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param input
|
|
||||||
*/
|
|
||||||
public constructor(input?: any) {
|
|
||||||
if (input && input.votes) {
|
|
||||||
input.votes.forEach(vote => {
|
|
||||||
if (vote.weight) {
|
|
||||||
vote.weight = parseFloat(vote.weight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
super(AssignmentPollOption.COLLECTIONSTRING, input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,18 @@
|
|||||||
import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service';
|
import { AssignmentOption } from './assignment-option';
|
||||||
import { AssignmentPollOption } from './assignment-poll-option';
|
import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll';
|
||||||
import { BaseModel } from '../base/base-model';
|
|
||||||
|
|
||||||
export interface AssignmentPollWithoutNestedModels extends BaseModel<AssignmentPoll> {
|
export enum AssignmentPollmethods {
|
||||||
id: number;
|
'yn' = 'yn',
|
||||||
pollmethod: AssignmentPollMethod;
|
'yna' = 'yna',
|
||||||
description: string;
|
'votes' = 'votes'
|
||||||
published: boolean;
|
}
|
||||||
votesvalid: number;
|
|
||||||
votesno: number;
|
export interface AssignmentPollWithoutNestedModels extends BasePollWithoutNestedModels {
|
||||||
votesabstain: number;
|
pollmethod: AssignmentPollmethods;
|
||||||
votesinvalid: number;
|
votes_amount: number;
|
||||||
votescast: number;
|
allow_multiple_votes_per_candidate: boolean;
|
||||||
has_votes: boolean;
|
global_no: boolean;
|
||||||
|
global_abstain: boolean;
|
||||||
assignment_id: number;
|
assignment_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,22 +20,12 @@ export interface AssignmentPollWithoutNestedModels extends BaseModel<AssignmentP
|
|||||||
* Content of the 'polls' property of assignments
|
* Content of the 'polls' property of assignments
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
export class AssignmentPoll extends BaseModel<AssignmentPoll> {
|
export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
||||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||||
private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain'];
|
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public options: AssignmentPollOption[];
|
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
// cast stringify numbers
|
|
||||||
if (input) {
|
|
||||||
AssignmentPoll.DECIMAL_FIELDS.forEach(field => {
|
|
||||||
if (input[field] && typeof input[field] === 'string') {
|
|
||||||
input[field] = parseFloat(input[field]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
super(AssignmentPoll.COLLECTIONSTRING, input);
|
super(AssignmentPoll.COLLECTIONSTRING, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
client/src/app/shared/models/assignments/assignment-vote.ts
Normal file
11
client/src/app/shared/models/assignments/assignment-vote.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { BaseVote } from '../poll/base-vote';
|
||||||
|
|
||||||
|
export class AssignmentVote extends BaseVote<AssignmentVote> {
|
||||||
|
public static COLLECTIONSTRING = 'assignments/assignment-vote';
|
||||||
|
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
public constructor(input?: any) {
|
||||||
|
super(AssignmentVote.COLLECTIONSTRING, input);
|
||||||
|
}
|
||||||
|
}
|
12
client/src/app/shared/models/base/base-decimal-model.ts
Normal file
12
client/src/app/shared/models/base/base-decimal-model.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { BaseModel } from './base-model';
|
||||||
|
|
||||||
|
export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
|
||||||
|
protected abstract decimalFields: (keyof this)[];
|
||||||
|
|
||||||
|
public deserialize(input: any): void {
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
this.decimalFields.forEach(field => (input[field] = parseInt(input[field], 10)));
|
||||||
|
}
|
||||||
|
super.deserialize(input);
|
||||||
|
}
|
||||||
|
}
|
9
client/src/app/shared/models/motions/motion-option.ts
Normal file
9
client/src/app/shared/models/motions/motion-option.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { BaseOption } from '../poll/base-option';
|
||||||
|
|
||||||
|
export class MotionOption extends BaseOption<MotionOption> {
|
||||||
|
public static COLLECTIONSTRING = 'motions/motion-option';
|
||||||
|
|
||||||
|
public constructor(input?: any) {
|
||||||
|
super(MotionOption.COLLECTIONSTRING, input);
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +1,26 @@
|
|||||||
import { Deserializer } from '../base/deserializer';
|
import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll';
|
||||||
|
import { MotionOption } from './motion-option';
|
||||||
|
|
||||||
|
export enum MotionPollmethods {
|
||||||
|
'YN' = 'YN',
|
||||||
|
'YNA' = 'YNA'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MotionPollWithoutNestedModels extends BasePollWithoutNestedModels {
|
||||||
|
motion_id: number;
|
||||||
|
pollmethod: MotionPollmethods;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a poll for a motion.
|
* Class representing a poll for a motion.
|
||||||
*/
|
*/
|
||||||
export class MotionPoll extends Deserializer {
|
export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
||||||
|
public static COLLECTIONSTRING = 'motions/motion-poll';
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public yes: number;
|
|
||||||
public no: number;
|
|
||||||
public abstain: number;
|
|
||||||
public votesvalid: number;
|
|
||||||
public votesinvalid: number;
|
|
||||||
public votescast: number;
|
|
||||||
public has_votes: boolean;
|
|
||||||
public motion_id: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Needs to be completely optional because motion has (yet) the optional parameter 'polls'
|
|
||||||
* Tries to cast incoming strings as numbers
|
|
||||||
* @param input
|
|
||||||
*/
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
if (typeof input === 'object') {
|
super(MotionPoll.COLLECTIONSTRING, input);
|
||||||
Object.keys(input).forEach(key => {
|
|
||||||
if (typeof input[key] === 'string') {
|
|
||||||
input[key] = parseInt(input[key], 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
super(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deserialize(input: any): void {
|
|
||||||
Object.assign(this, input);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export interface MotionPoll extends MotionPollWithoutNestedModels {}
|
||||||
|
11
client/src/app/shared/models/motions/motion-vote.ts
Normal file
11
client/src/app/shared/models/motions/motion-vote.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { BaseVote } from '../poll/base-vote';
|
||||||
|
|
||||||
|
export class MotionVote extends BaseVote<MotionVote> {
|
||||||
|
public static COLLECTIONSTRING = 'motions/motion-vote';
|
||||||
|
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
public constructor(input?: any) {
|
||||||
|
super(MotionVote.COLLECTIONSTRING, input);
|
||||||
|
}
|
||||||
|
}
|
11
client/src/app/shared/models/poll/base-option.ts
Normal file
11
client/src/app/shared/models/poll/base-option.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||||
|
|
||||||
|
export abstract class BaseOption<T> extends BaseDecimalModel<T> {
|
||||||
|
public id: number;
|
||||||
|
public yes: number;
|
||||||
|
public no: number;
|
||||||
|
public abstain: number;
|
||||||
|
public votes_id: number[];
|
||||||
|
|
||||||
|
protected decimalFields: (keyof BaseOption<T>)[] = ['yes', 'no', 'abstain'];
|
||||||
|
}
|
33
client/src/app/shared/models/poll/base-poll.ts
Normal file
33
client/src/app/shared/models/poll/base-poll.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||||
|
import { BaseOption } from './base-option';
|
||||||
|
|
||||||
|
export enum PollState {
|
||||||
|
Created = 1,
|
||||||
|
Started,
|
||||||
|
Finished,
|
||||||
|
Published
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PollType {
|
||||||
|
Analog = 'analog',
|
||||||
|
Named = 'named',
|
||||||
|
Pseudoanonymous = 'pseudoanonymous'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasePollWithoutNestedModels {
|
||||||
|
state: PollState;
|
||||||
|
type: PollType;
|
||||||
|
title: string;
|
||||||
|
votesvalid: number;
|
||||||
|
votesinvalid: number;
|
||||||
|
votescast: number;
|
||||||
|
groups_id: number[];
|
||||||
|
voted_id: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BasePoll<T, O extends BaseOption<any>> extends BaseDecimalModel<T> {
|
||||||
|
public options: O[];
|
||||||
|
|
||||||
|
protected decimalFields: (keyof BasePoll<T, O>)[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
|
}
|
||||||
|
export interface BasePoll<T, O extends BaseOption<any>> extends BasePollWithoutNestedModels {}
|
9
client/src/app/shared/models/poll/base-vote.ts
Normal file
9
client/src/app/shared/models/poll/base-vote.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||||
|
|
||||||
|
export abstract class BaseVote<T> extends BaseDecimalModel<T> {
|
||||||
|
public weight: number;
|
||||||
|
public value: 'Y' | 'N' | 'A';
|
||||||
|
public user_id?: number;
|
||||||
|
|
||||||
|
protected decimalFields: (keyof BaseVote<T>)[] = ['weight'];
|
||||||
|
}
|
@ -9,9 +9,8 @@ import { ViewListOfSpeakers } from './models/view-list-of-speakers';
|
|||||||
export const AgendaAppConfig: AppConfig = {
|
export const AgendaAppConfig: AppConfig = {
|
||||||
name: 'agenda',
|
name: 'agenda',
|
||||||
models: [
|
models: [
|
||||||
{ collectionString: 'agenda/item', model: Item, viewModel: ViewItem, repository: ItemRepositoryService },
|
{ model: Item, viewModel: ViewItem, repository: ItemRepositoryService },
|
||||||
{
|
{
|
||||||
collectionString: 'agenda/list-of-speakers',
|
|
||||||
model: ListOfSpeakers,
|
model: ListOfSpeakers,
|
||||||
viewModel: ViewListOfSpeakers,
|
viewModel: ViewListOfSpeakers,
|
||||||
repository: ListOfSpeakersRepositoryService
|
repository: ListOfSpeakersRepositoryService
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
import { AppConfig } from '../../core/definitions/app-config';
|
import { AppConfig } from '../../core/definitions/app-config';
|
||||||
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
|
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
||||||
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
import { Assignment } from '../../shared/models/assignments/assignment';
|
import { Assignment } from '../../shared/models/assignments/assignment';
|
||||||
import { ViewAssignment } from './models/view-assignment';
|
import { ViewAssignment } from './models/view-assignment';
|
||||||
|
import { ViewAssignmentPoll } from './models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentVote } from './models/view-assignment-vote';
|
||||||
|
|
||||||
export const AssignmentsAppConfig: AppConfig = {
|
export const AssignmentsAppConfig: AppConfig = {
|
||||||
name: 'assignments',
|
name: 'assignments',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'assignments/assignment',
|
|
||||||
model: Assignment,
|
model: Assignment,
|
||||||
viewModel: ViewAssignment,
|
viewModel: ViewAssignment,
|
||||||
// searchOrder: 3, // TODO: enable, if there is a detail page and so on.
|
// searchOrder: 3, // TODO: enable, if there is a detail page and so on.
|
||||||
repository: AssignmentRepositoryService
|
repository: AssignmentRepositoryService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: AssignmentPoll,
|
||||||
|
viewModel: ViewAssignmentPoll,
|
||||||
|
repository: AssignmentPollRepositoryService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: AssignmentVote,
|
||||||
|
viewModel: ViewAssignmentVote,
|
||||||
|
repository: AssignmentVoteRepositoryService
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mainMenuEntries: [
|
mainMenuEntries: [
|
||||||
|
@ -147,7 +147,7 @@
|
|||||||
<!-- TODO avoid animation/switching on update -->
|
<!-- TODO avoid animation/switching on update -->
|
||||||
<mat-tab
|
<mat-tab
|
||||||
*ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex"
|
*ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex"
|
||||||
[label]="getPollLabel(poll, i)"
|
[label]="poll.title"
|
||||||
>
|
>
|
||||||
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
@ -15,7 +15,6 @@ import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.
|
|||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
|
||||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
@ -23,7 +22,6 @@ import { LocalPermissionsService } from 'app/site/motions/services/local-permiss
|
|||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
|
||||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||||
|
|
||||||
@ -171,7 +169,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
formBuilder: FormBuilder,
|
formBuilder: FormBuilder,
|
||||||
public repo: AssignmentRepositoryService,
|
public repo: AssignmentRepositoryService,
|
||||||
private userRepo: UserRepositoryService,
|
private userRepo: UserRepositoryService,
|
||||||
public pollService: AssignmentPollService,
|
|
||||||
private itemRepo: ItemRepositoryService,
|
private itemRepo: ItemRepositoryService,
|
||||||
private tagRepo: TagRepositoryService,
|
private tagRepo: TagRepositoryService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
@ -303,10 +300,9 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Poll
|
* Creates a new Poll
|
||||||
* TODO: directly open poll dialog?
|
|
||||||
*/
|
*/
|
||||||
public async createPoll(): Promise<void> {
|
public async createPoll(): Promise<void> {
|
||||||
await this.repo.addPoll(this.assignment).catch(this.raiseError);
|
// await this.repo.createPoll(this.assignment).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -455,24 +451,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assemble a meaningful label for the poll
|
|
||||||
* Published polls will look like 'Ballot 2'
|
|
||||||
* other polls will be named 'Ballot 2' for normal users, with the hint
|
|
||||||
* '(unpulished)' appended for manager users
|
|
||||||
*
|
|
||||||
* @param poll
|
|
||||||
* @param index the index of the poll relative to the assignment
|
|
||||||
*/
|
|
||||||
public getPollLabel(poll: AssignmentPoll, index: number): string {
|
|
||||||
const title = `${this.translate.instant('Ballot')} ${index + 1}`;
|
|
||||||
if (!poll.published && this.hasPerms('manage')) {
|
|
||||||
return title + ` (${this.translate.instant('unpublished')})`;
|
|
||||||
} else {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers an update of the filter for the list of available candidates
|
* Triggers an update of the filter for the list of available candidates
|
||||||
* (triggered on an autoupdate of either users or the assignment)
|
* (triggered on an autoupdate of either users or the assignment)
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
|
||||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
|
||||||
import { AssignmentPollService, SummaryPollKey } from '../../services/assignment-poll.service';
|
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vote entries included once for summary (e.g. total votes cast)
|
* Vote entries included once for summary (e.g. total votes cast)
|
||||||
@ -61,16 +54,9 @@ export class AssignmentPollDialogComponent {
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll,
|
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll
|
||||||
private matSnackBar: MatSnackBar,
|
|
||||||
private translate: TranslateService,
|
|
||||||
public pollService: AssignmentPollService,
|
|
||||||
private userRepo: UserRepositoryService
|
|
||||||
) {
|
) {
|
||||||
this.specialValues = this.pollService.specialPollVotes;
|
switch (this.data.pollmethod) {
|
||||||
this.poll = this.data.poll;
|
|
||||||
|
|
||||||
switch (this.poll.pollmethod) {
|
|
||||||
case 'votes':
|
case 'votes':
|
||||||
this.optionPollKeys = ['Votes'];
|
this.optionPollKeys = ['Votes'];
|
||||||
break;
|
break;
|
||||||
@ -97,7 +83,7 @@ export class AssignmentPollDialogComponent {
|
|||||||
* TODO better validation
|
* TODO better validation
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
const error = this.data.options.find(dataoption => {
|
/*const error = this.data.options.find(dataoption => {
|
||||||
this.optionPollKeys.some(key => {
|
this.optionPollKeys.some(key => {
|
||||||
const keyValue = dataoption.votes.find(o => o.value === key);
|
const keyValue = dataoption.votes.find(o => o.value === key);
|
||||||
return !keyValue || keyValue.weight === undefined;
|
return !keyValue || keyValue.weight === undefined;
|
||||||
@ -112,8 +98,8 @@ export class AssignmentPollDialogComponent {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.dialogRef.close(this.poll);
|
this.dialogRef.close(this.data);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,7 +109,8 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @returns a label for a poll option
|
* @returns a label for a poll option
|
||||||
*/
|
*/
|
||||||
public getLabel(key: CalculablePollKey): string {
|
public getLabel(key: CalculablePollKey): string {
|
||||||
return this.pollService.getLabel(key);
|
// return this.pollService.getLabel(key);
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,8 +120,8 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param candidate the candidate for whom to update the value
|
* @param candidate the candidate for whom to update the value
|
||||||
* @param newData the new value
|
* @param newData the new value
|
||||||
*/
|
*/
|
||||||
public setValue(value: PollVoteValue, candidate: ViewAssignmentPollOption, newData: string): void {
|
public setValue(value: PollVoteValue, candidate: ViewAssignmentOption, newData: string): void {
|
||||||
const vote = candidate.votes.find(v => v.value === value);
|
/*const vote = candidate.votes.find(v => v.value === value);
|
||||||
if (vote) {
|
if (vote) {
|
||||||
vote.weight = parseFloat(newData);
|
vote.weight = parseFloat(newData);
|
||||||
} else {
|
} else {
|
||||||
@ -142,7 +129,7 @@ export class AssignmentPollDialogComponent {
|
|||||||
value: value,
|
value: value,
|
||||||
weight: parseFloat(newData)
|
weight: parseFloat(newData)
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,9 +139,10 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param candidate the pollOption
|
* @param candidate the pollOption
|
||||||
* @returns the currently entered number or undefined if no number has been set
|
* @returns the currently entered number or undefined if no number has been set
|
||||||
*/
|
*/
|
||||||
public getValue(value: PollVoteValue, candidate: AssignmentPollOption): number | undefined {
|
public getValue(value: PollVoteValue, candidate: ViewAssignmentOption): number | undefined {
|
||||||
const val = candidate.votes.find(v => v.value === value);
|
/*const val = candidate.votes.find(v => v.value === value);
|
||||||
return val ? val.weight : undefined;
|
return val ? val.weight : undefined;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,8 +151,9 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param value
|
* @param value
|
||||||
* @returns integer or undefined
|
* @returns integer or undefined
|
||||||
*/
|
*/
|
||||||
public getSumValue(value: SummaryPollKey): number | undefined {
|
public getSumValue(value: any /*SummaryPollKey*/): number | undefined {
|
||||||
return this.data[value] || undefined;
|
// return this.data[value] || undefined;
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,23 +162,11 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param value
|
* @param value
|
||||||
* @param weight
|
* @param weight
|
||||||
*/
|
*/
|
||||||
public setSumValue(value: SummaryPollKey, weight: string): void {
|
public setSumValue(value: any /*SummaryPollKey*/, weight: string): void {
|
||||||
this.poll[value] = parseFloat(weight);
|
this.data[value] = parseFloat(weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGridClass(): string {
|
public getGridClass(): string {
|
||||||
return `votes-grid-${this.optionPollKeys.length}`;
|
return `votes-grid-${this.optionPollKeys.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the name for a poll option
|
|
||||||
* TODO: observable. Note that the assignment.related_user may not contain the user (anymore?)
|
|
||||||
*
|
|
||||||
* @param option Any poll option
|
|
||||||
* @returns the full_name for the candidate
|
|
||||||
*/
|
|
||||||
public getCandidateName(option: AssignmentPollOption): string {
|
|
||||||
const user = this.userRepo.getViewModel(option.candidate_id);
|
|
||||||
return user ? user.full_name : '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
@ -7,17 +7,12 @@ import { Title } from '@angular/platform-browser';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
|
||||||
import { CalculablePollKey, MajorityMethod } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey, MajorityMethod } from 'app/core/ui-services/poll.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
|
||||||
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component';
|
|
||||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
|
||||||
import { ViewAssignment } from '../../models/view-assignment';
|
import { ViewAssignment } from '../../models/view-assignment';
|
||||||
|
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for a single assignment poll. Used in assignment detail view
|
* Component for a single assignment poll. Used in assignment detail view
|
||||||
@ -69,24 +64,27 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* used in this poll (e.g.)
|
* used in this poll (e.g.)
|
||||||
*/
|
*/
|
||||||
public get pollValues(): CalculablePollKey[] {
|
public get pollValues(): CalculablePollKey[] {
|
||||||
return this.pollService.getVoteOptionsByPoll(this.poll);
|
// return this.pollService.getVoteOptionsByPoll(this.poll);
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if the description on the form differs from the poll's description
|
* @returns true if the description on the form differs from the poll's description
|
||||||
*/
|
*/
|
||||||
public get dirtyDescription(): boolean {
|
public get dirtyDescription(): boolean {
|
||||||
return this.descriptionForm.get('description').value !== this.poll.description;
|
// return this.descriptionForm.get('description').value !== this.poll.description;
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if vote results can be seen by the user
|
* @returns true if vote results can be seen by the user
|
||||||
*/
|
*/
|
||||||
public get pollData(): boolean {
|
public get pollData(): boolean {
|
||||||
if (!this.poll.has_votes) {
|
/*if (!this.poll.has_votes) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.poll.published || this.canManage;
|
return this.poll.published || this.canManage;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,29 +111,12 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor. Does nothing
|
|
||||||
*
|
|
||||||
* @param titleService
|
|
||||||
* @param matSnackBar
|
|
||||||
* @param pollService poll related calculations
|
|
||||||
* @param operator permission checks
|
|
||||||
* @param assignmentRepo The repository to the assignments
|
|
||||||
* @param translate Translation service
|
|
||||||
* @param dialog MatDialog for the vote entering dialog
|
|
||||||
* @param promptService Prompts for confirmation dialogs
|
|
||||||
* @param pdfService pdf service
|
|
||||||
*/
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
public pollService: AssignmentPollService,
|
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private assignmentRepo: AssignmentRepositoryService,
|
|
||||||
public translate: TranslateService,
|
public translate: TranslateService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private promptService: PromptService,
|
|
||||||
private formBuilder: FormBuilder,
|
|
||||||
private pdfService: AssignmentPollPdfService
|
private pdfService: AssignmentPollPdfService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
@ -145,12 +126,12 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* Gets the currently selected majority choice option from the repo
|
* Gets the currently selected majority choice option from the repo
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.majorityChoice =
|
/*this.majorityChoice =
|
||||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||||
null;
|
null;
|
||||||
this.descriptionForm = this.formBuilder.group({
|
this.descriptionForm = this.formBuilder.group({
|
||||||
description: this.poll ? this.poll.description : ''
|
description: this.poll ? this.poll.description : ''
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,10 +140,10 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* TODO: Some confirmation (advanced logic (e.g. not deleting published?))
|
* TODO: Some confirmation (advanced logic (e.g. not deleting published?))
|
||||||
*/
|
*/
|
||||||
public async onDeletePoll(): Promise<void> {
|
public async onDeletePoll(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this ballot?');
|
/*const title = this.translate.instant('Are you sure you want to delete this ballot?');
|
||||||
if (await this.promptService.open(title)) {
|
if (await this.promptService.open(title)) {
|
||||||
await this.assignmentRepo.deletePoll(this.poll).catch(this.raiseError);
|
await this.assignmentRepo.deletePoll(this.poll).catch(this.raiseError);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,15 +161,16 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* @param option
|
* @param option
|
||||||
* @returns true if the quorum is successfully met
|
* @returns true if the quorum is successfully met
|
||||||
*/
|
*/
|
||||||
public quorumReached(option: ViewAssignmentPollOption): boolean {
|
public quorumReached(option: ViewAssignmentOption): boolean {
|
||||||
const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
|
/*const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
|
||||||
const amount = option.votes.find(v => v.value === yesValue).weight;
|
const amount = option.votes.find(v => v.value === yesValue).weight;
|
||||||
const yesQuorum = this.pollService.yesQuorum(
|
const yesQuorum = this.pollService.yesQuorum(
|
||||||
this.majorityChoice,
|
this.majorityChoice,
|
||||||
this.pollService.calculationDataFromPoll(this.poll),
|
this.pollService.calculationDataFromPoll(this.poll),
|
||||||
option
|
option
|
||||||
);
|
);
|
||||||
return yesQuorum && amount >= yesQuorum;
|
return yesQuorum && amount >= yesQuorum;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,15 +178,15 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* closes successfully (validation is done there)
|
* closes successfully (validation is done there)
|
||||||
*/
|
*/
|
||||||
public enterVotes(): void {
|
public enterVotes(): void {
|
||||||
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
/*const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
||||||
data: this.poll,
|
data: this.poll.copy(),
|
||||||
...mediumDialogSettings
|
...mediumDialogSettings
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.assignmentRepo.updateVotes(result, this.poll).catch(this.raiseError);
|
this.assignmentRepo.updateVotes(result, this.poll).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,7 +202,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* Toggles the 'published' state
|
* Toggles the 'published' state
|
||||||
*/
|
*/
|
||||||
public togglePublished(): void {
|
public togglePublished(): void {
|
||||||
this.assignmentRepo.updatePoll({ published: !this.poll.published }, this.poll);
|
// this.assignmentRepo.updatePoll({ published: !this.poll.published }, this.poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,8 +210,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
*
|
*
|
||||||
* @param option
|
* @param option
|
||||||
*/
|
*/
|
||||||
public toggleElected(option: ViewAssignmentPollOption): void {
|
public toggleElected(option: ViewAssignmentOption): void {
|
||||||
if (!this.operator.hasPerms('assignments.can_manage')) {
|
/*if (!this.operator.hasPerms('assignments.can_manage')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +221,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
);
|
);
|
||||||
if (viewAssignmentRelatedUser) {
|
if (viewAssignmentRelatedUser) {
|
||||||
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
|
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,8 +229,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* TODO: Better feedback
|
* TODO: Better feedback
|
||||||
*/
|
*/
|
||||||
public async onEditDescriptionButton(): Promise<void> {
|
public async onEditDescriptionButton(): Promise<void> {
|
||||||
const desc: string = this.descriptionForm.get('description').value;
|
/*const desc: string = this.descriptionForm.get('description').value;
|
||||||
await this.assignmentRepo.updatePoll({ description: desc }, this.poll).catch(this.raiseError);
|
await this.assignmentRepo.updatePoll({ description: desc }, this.poll).catch(this.raiseError);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -256,8 +238,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* @param option
|
* @param option
|
||||||
* @returns a translated
|
* @returns a translated
|
||||||
*/
|
*/
|
||||||
public getQuorumReachedString(option: ViewAssignmentPollOption): string {
|
public getQuorumReachedString(option: ViewAssignmentOption): string {
|
||||||
const name = this.translate.instant(this.majorityChoice.display_name);
|
/*const name = this.translate.instant(this.majorityChoice.display_name);
|
||||||
const quorum = this.pollService.yesQuorum(
|
const quorum = this.pollService.yesQuorum(
|
||||||
this.majorityChoice,
|
this.majorityChoice,
|
||||||
this.pollService.calculationDataFromPoll(this.poll),
|
this.pollService.calculationDataFromPoll(this.poll),
|
||||||
@ -266,6 +248,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
const isReached = this.quorumReached(option)
|
const isReached = this.quorumReached(option)
|
||||||
? this.translate.instant('reached')
|
? this.translate.instant('reached')
|
||||||
: this.translate.instant('not reached');
|
: this.translate.instant('not reached');
|
||||||
return `${name} (${quorum}) ${isReached}`;
|
return `${name} (${quorum}) ${isReached}`;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
|
||||||
|
export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
||||||
|
public get option(): AssignmentOption {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
public static COLLECTIONSTRING = AssignmentOption.COLLECTIONSTRING;
|
||||||
|
protected _collectionString = AssignmentOption.COLLECTIONSTRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewAssignmentOption extends AssignmentOption {}
|
@ -1,25 +0,0 @@
|
|||||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
|
||||||
import { AssignmentOptionVote, AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the order the option's votes are sorted in (server might send raw data in any order)
|
|
||||||
*/
|
|
||||||
const votesOrder: PollVoteValue[] = ['Votes', 'Yes', 'No', 'Abstain'];
|
|
||||||
|
|
||||||
export class ViewAssignmentPollOption extends BaseViewModel<AssignmentPollOption> {
|
|
||||||
public static COLLECTIONSTRING = AssignmentPollOption.COLLECTIONSTRING;
|
|
||||||
protected _collectionString = AssignmentPollOption.COLLECTIONSTRING;
|
|
||||||
|
|
||||||
public get option(): AssignmentPollOption {
|
|
||||||
return this._model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get votes(): AssignmentOptionVote[] {
|
|
||||||
return this.option.votes.sort((a, b) => votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface ViewAssignmentPollOption extends AssignmentPollOption {
|
|
||||||
user: ViewUser;
|
|
||||||
}
|
|
@ -1,9 +1,16 @@
|
|||||||
import { AssignmentPoll, AssignmentPollWithoutNestedModels } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll, AssignmentPollWithoutNestedModels } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewAssignmentPollOption } from './view-assignment-poll-option';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { ViewAssignmentOption } from './view-assignment-option';
|
||||||
|
|
||||||
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll> {
|
export interface AssignmentPollTitleInformation {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
||||||
|
implements AssignmentPollTitleInformation {
|
||||||
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
|
||||||
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
|
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
@ -11,18 +18,10 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
|||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getListTitle = () => {
|
|
||||||
return this.getTitle();
|
|
||||||
};
|
|
||||||
|
|
||||||
public getProjectorTitle = () => {
|
|
||||||
return this.getTitle();
|
|
||||||
};
|
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
return {
|
/*return {
|
||||||
getBasicProjectorElement: options => ({
|
getBasicProjectorElement: options => ({
|
||||||
name: 'assignments/poll',
|
name: 'assignments/assignment-poll',
|
||||||
assignment_id: this.assignment_id,
|
assignment_id: this.assignment_id,
|
||||||
poll_id: this.id,
|
poll_id: this.id,
|
||||||
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
|
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
|
||||||
@ -30,10 +29,15 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
|||||||
slideOptions: [],
|
slideOptions: [],
|
||||||
projectionDefaultName: 'assignments',
|
projectionDefaultName: 'assignments',
|
||||||
getDialogTitle: () => 'TODO'
|
getDialogTitle: () => 'TODO'
|
||||||
};
|
};*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels {
|
interface TIAssignmentPollRelations {
|
||||||
options: ViewAssignmentPollOption[];
|
options: ViewAssignmentOption[];
|
||||||
|
voted: ViewUser[];
|
||||||
|
groups: ViewGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels, TIAssignmentPollRelations {}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
|
||||||
|
export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
||||||
|
public get vote(): AssignmentVote {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
public static COLLECTIONSTRING = AssignmentVote.COLLECTIONSTRING;
|
||||||
|
protected _collectionString = AssignmentVote.COLLECTIONSTRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TIAssignmentVoteRelations {
|
||||||
|
user?: ViewUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewAssignmentVote extends AssignmentVote, TIAssignmentVoteRelations {}
|
@ -45,9 +45,6 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
|
|||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Fix assignment creation: DO NOT create a ViewUser there...
|
|
||||||
*/
|
|
||||||
public get candidates(): ViewUser[] {
|
public get candidates(): ViewUser[] {
|
||||||
if (!this.assignment_related_users) {
|
if (!this.assignment_related_users) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -3,11 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { HtmlToPdfService } from 'app/core/pdf-services/html-to-pdf.service';
|
import { HtmlToPdfService } from 'app/core/pdf-services/html-to-pdf.service';
|
||||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
|
||||||
import { AssignmentPollService } from './assignment-poll.service';
|
|
||||||
import { ViewAssignment } from '../models/view-assignment';
|
import { ViewAssignment } from '../models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
|
||||||
import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a PDF document from a single assignment
|
* Creates a PDF document from a single assignment
|
||||||
@ -16,12 +12,6 @@ import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option'
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AssignmentPdfService {
|
export class AssignmentPdfService {
|
||||||
/**
|
|
||||||
* Will be set to `true` of a person was elected.
|
|
||||||
* Determines that in indicator is shown under the table
|
|
||||||
*/
|
|
||||||
private showIsElected = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@ -30,11 +20,7 @@ export class AssignmentPdfService {
|
|||||||
* @param pdfDocumentService PDF functions
|
* @param pdfDocumentService PDF functions
|
||||||
* @param htmlToPdfService Convert the assignment detail html text to pdf
|
* @param htmlToPdfService Convert the assignment detail html text to pdf
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(private translate: TranslateService, private htmlToPdfService: HtmlToPdfService) {}
|
||||||
private translate: TranslateService,
|
|
||||||
private pollService: AssignmentPollService,
|
|
||||||
private htmlToPdfService: HtmlToPdfService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main function to control the pdf generation.
|
* Main function to control the pdf generation.
|
||||||
@ -161,7 +147,8 @@ export class AssignmentPdfService {
|
|||||||
* @param pollOption the poll options (yes, no, maybe [...])
|
* @param pollOption the poll options (yes, no, maybe [...])
|
||||||
* @returns a line in the table
|
* @returns a line in the table
|
||||||
*/
|
*/
|
||||||
private electedCandidateLine(candidateName: string, pollOption: ViewAssignmentPollOption): object {
|
// TODO: type the result.
|
||||||
|
/*private electedCandidateLine(candidateName: string, pollOption: ViewAssignmentOption): object {
|
||||||
if (pollOption.is_elected) {
|
if (pollOption.is_elected) {
|
||||||
this.showIsElected = true;
|
this.showIsElected = true;
|
||||||
return {
|
return {
|
||||||
@ -172,8 +159,8 @@ export class AssignmentPdfService {
|
|||||||
return {
|
return {
|
||||||
text: candidateName
|
text: candidateName
|
||||||
};
|
};
|
||||||
}
|
}*
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the poll result table for all published polls
|
* Creates the poll result table for all published polls
|
||||||
@ -181,8 +168,9 @@ export class AssignmentPdfService {
|
|||||||
* @param assignment the ViewAssignment to create the document for
|
* @param assignment the ViewAssignment to create the document for
|
||||||
* @returns the table as pdfmake object
|
* @returns the table as pdfmake object
|
||||||
*/
|
*/
|
||||||
|
// TODO: type the result
|
||||||
private createPollResultTable(assignment: ViewAssignment): object {
|
private createPollResultTable(assignment: ViewAssignment): object {
|
||||||
const resultBody = [];
|
/*const resultBody = [];
|
||||||
for (let pollIndex = 0; pollIndex < assignment.polls.length; pollIndex++) {
|
for (let pollIndex = 0; pollIndex < assignment.polls.length; pollIndex++) {
|
||||||
const poll = assignment.polls[pollIndex];
|
const poll = assignment.polls[pollIndex];
|
||||||
if (poll.published) {
|
if (poll.published) {
|
||||||
@ -277,7 +265,8 @@ export class AssignmentPdfService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultBody;
|
return resultBody;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -287,32 +276,32 @@ export class AssignmentPdfService {
|
|||||||
* @param optionLabel Usually Yes or No
|
* @param optionLabel Usually Yes or No
|
||||||
* @param value the amount of votes
|
* @param value the amount of votes
|
||||||
* @param poll the specific poll
|
* @param poll the specific poll
|
||||||
* @param pollOption the corresponding poll option
|
* @param option the corresponding poll option
|
||||||
* @returns a string a nicer number representation: "Yes 25 (22,2%)" or just "10"
|
* @returns a string a nicer number representation: "Yes 25 (22,2%)" or just "10"
|
||||||
*/
|
*/
|
||||||
private parseVoteValue(
|
/*private parseVoteValue(
|
||||||
optionLabel: PollVoteValue,
|
optionLabel: PollVoteValue,
|
||||||
value: number,
|
value: number,
|
||||||
poll: ViewAssignmentPoll,
|
poll: ViewAssignmentPoll,
|
||||||
pollOption: ViewAssignmentPollOption
|
option: ViewAssignmentOption
|
||||||
): string {
|
): string {
|
||||||
let resultString = '';
|
let resultString = '';
|
||||||
const label = this.translate.instant(this.pollService.getLabel(optionLabel));
|
const label = this.translate.instant(this.pollService.getLabel(optionLabel));
|
||||||
const valueString = this.pollService.getSpecialLabel(value);
|
const valueString = this.pollService.getSpecialLabel(value);
|
||||||
const percentNr = this.pollService.getPercent(
|
const percentNr = this.pollService.getPercent(
|
||||||
this.pollService.calculationDataFromPoll(poll),
|
this.pollService.calculationDataFromPoll(poll),
|
||||||
pollOption,
|
option,
|
||||||
optionLabel
|
optionLabel
|
||||||
);
|
);
|
||||||
|
|
||||||
resultString += `${label} ${valueString}`;
|
resultString += `${label} ${valueString}`;
|
||||||
if (
|
if (
|
||||||
percentNr &&
|
percentNr &&
|
||||||
!this.pollService.isAbstractOption(this.pollService.calculationDataFromPoll(poll), pollOption, optionLabel)
|
!this.pollService.isAbstractOption(this.pollService.calculationDataFromPoll(poll), option, optionLabel)
|
||||||
) {
|
) {
|
||||||
resultString += ` (${percentNr}%)`;
|
resultString += ` (${percentNr}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${resultString}\n`;
|
return `${resultString}\n`;
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
|
|||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { AssignmentPollMethod } from './assignment-poll.service';
|
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,6 +112,7 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
* @param title The identifier of the motion
|
* @param title The identifier of the motion
|
||||||
* @param subtitle The actual motion title
|
* @param subtitle The actual motion title
|
||||||
*/
|
*/
|
||||||
|
// TODO: typing of result
|
||||||
protected createBallot(data: AbstractPollData): object {
|
protected createBallot(data: AbstractPollData): object {
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
@ -136,8 +136,9 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: typing of result
|
||||||
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
||||||
const candidates = poll.options.sort((a, b) => {
|
/*const candidates = poll.options.sort((a, b) => {
|
||||||
return a.weight - b.weight;
|
return a.weight - b.weight;
|
||||||
});
|
});
|
||||||
const resultObject = candidates.map(cand => {
|
const resultObject = candidates.map(cand => {
|
||||||
@ -150,10 +151,12 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
noEntry.margin[1] = 25;
|
noEntry.margin[1] = 25;
|
||||||
resultObject.push(noEntry);
|
resultObject.push(noEntry);
|
||||||
}
|
}
|
||||||
return resultObject;
|
return resultObject;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
private createYNBallotEntry(option: string, method: AssignmentPollMethod): object {
|
// TODO: typing of result
|
||||||
|
/*private createYNBallotEntry(option: string, method: AssignmentPollmethods): object {
|
||||||
const choices = method === 'yna' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
const choices = method === 'yna' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
||||||
const columnstack = choices.map(choice => {
|
const columnstack = choices.map(choice => {
|
||||||
return {
|
return {
|
||||||
@ -171,7 +174,7 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
columns: columnstack
|
columns: columnstack
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the poll description
|
* Generates the poll description
|
||||||
@ -179,10 +182,12 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
* @param poll
|
* @param poll
|
||||||
* @returns pdfMake definitions
|
* @returns pdfMake definitions
|
||||||
*/
|
*/
|
||||||
|
// TODO: typing of result
|
||||||
private createPollHint(poll: ViewAssignmentPoll): object {
|
private createPollHint(poll: ViewAssignmentPoll): object {
|
||||||
return {
|
/*return {
|
||||||
text: poll.description || '',
|
text: poll.description || '',
|
||||||
style: 'description'
|
style: 'description'
|
||||||
};
|
};*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AssignmentPollService } from './assignment-poll.service';
|
|
||||||
|
|
||||||
describe('PollService', () => {
|
|
||||||
beforeEach(() => TestBed.configureTestingModule({}));
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
const service: AssignmentPollService = TestBed.get(AssignmentPollService);
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,308 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
|
||||||
import {
|
|
||||||
CalculablePollKey,
|
|
||||||
MajorityMethod,
|
|
||||||
PollMajorityMethod,
|
|
||||||
PollService,
|
|
||||||
PollVoteValue
|
|
||||||
} from 'app/core/ui-services/poll.service';
|
|
||||||
import { AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option';
|
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
|
||||||
import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option';
|
|
||||||
|
|
||||||
type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno';
|
|
||||||
export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
|
|
||||||
export type AssignmentPercentBase = 'YES_NO_ABSTAIN' | 'YES_NO' | 'VALID' | 'CAST' | 'DISABLED';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* interface common to data in a ViewAssignmentPoll and PollSlideData
|
|
||||||
*
|
|
||||||
* TODO: simplify
|
|
||||||
*/
|
|
||||||
export interface CalculationData {
|
|
||||||
pollMethod: AssignmentPollMethod;
|
|
||||||
votesno: number;
|
|
||||||
votesabstain: number;
|
|
||||||
votescast: number;
|
|
||||||
votesvalid: number;
|
|
||||||
votesinvalid: number;
|
|
||||||
percentBase?: AssignmentPercentBase;
|
|
||||||
pollOptions?: {
|
|
||||||
votes: AssignmentOptionVote[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CalculationOption {
|
|
||||||
votes: AssignmentOptionVote[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vote entries included once for summary (e.g. total votes cast)
|
|
||||||
*/
|
|
||||||
export type SummaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid' | 'votesno' | 'votesabstain';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service class for assignment polls.
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AssignmentPollService extends PollService {
|
|
||||||
/**
|
|
||||||
* list of poll keys that are numbers and can be part of a quorum calculation
|
|
||||||
*/
|
|
||||||
public pollValues: CalculablePollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the method used for polls (as per config)
|
|
||||||
*/
|
|
||||||
public pollMethod: AssignmentPollValues;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the method used to determine the '100%' base (set in config)
|
|
||||||
*/
|
|
||||||
public percentBase: AssignmentPercentBase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convenience function for displaying the available majorities
|
|
||||||
*/
|
|
||||||
public get majorityMethods(): MajorityMethod[] {
|
|
||||||
return PollMajorityMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor. Subscribes to the configuration values needed
|
|
||||||
*
|
|
||||||
* @param config ConfigService
|
|
||||||
*/
|
|
||||||
public constructor(config: ConfigService) {
|
|
||||||
super();
|
|
||||||
config
|
|
||||||
.get<string>('assignments_poll_default_majority_method')
|
|
||||||
.subscribe(method => (this.defaultMajorityMethod = method));
|
|
||||||
config
|
|
||||||
.get<AssignmentPollValues>('assignments_poll_vote_values')
|
|
||||||
.subscribe(method => (this.pollMethod = method));
|
|
||||||
config
|
|
||||||
.get<AssignmentPercentBase>('assignments_poll_100_percent_base')
|
|
||||||
.subscribe(base => (this.percentBase = base));
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVoteOptionsByPoll(poll: ViewAssignmentPoll): CalculablePollKey[] {
|
|
||||||
return this.pollValues.filter(name => poll[name] !== undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base amount for the 100% calculations. Note that some poll methods
|
|
||||||
* (e.g. yes/no/abstain may have a different percentage base and will return null here)
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @returns The amount of votes indicating the 100% base
|
|
||||||
*/
|
|
||||||
public getBaseAmount(data: CalculationData): number | null {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
switch (percentBase) {
|
|
||||||
case 'DISABLED':
|
|
||||||
return null;
|
|
||||||
case 'YES_NO':
|
|
||||||
case 'YES_NO_ABSTAIN':
|
|
||||||
if (data.pollMethod === 'votes') {
|
|
||||||
const yes = data.pollOptions.map(option => {
|
|
||||||
const yesValue = option.votes.find(v => v.value === 'Votes');
|
|
||||||
return yesValue ? yesValue.weight : -99;
|
|
||||||
});
|
|
||||||
if (Math.min(...yes) < 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
// TODO: Counting 'No (and possibly 'Abstain') here?
|
|
||||||
return yes.reduce((a, b) => a + b);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case 'CAST':
|
|
||||||
return data.votescast > 0 && data.votesinvalid >= 0 ? data.votescast : null;
|
|
||||||
case 'VALID':
|
|
||||||
return data.votesvalid > 0 ? data.votesvalid : null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the percentage for an option
|
|
||||||
*
|
|
||||||
* @param poll
|
|
||||||
* @param data
|
|
||||||
* @returns a percentage number with two digits, null if the value cannot be calculated
|
|
||||||
*/
|
|
||||||
public getPercent(data: CalculationData, option: CalculationOption, key: PollVoteValue): number | null {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
let base = 0;
|
|
||||||
if (percentBase === 'DISABLED') {
|
|
||||||
return null;
|
|
||||||
} else if (percentBase === 'VALID') {
|
|
||||||
base = data.votesvalid;
|
|
||||||
} else if (percentBase === 'CAST') {
|
|
||||||
base = data.votescast;
|
|
||||||
} else {
|
|
||||||
base = data.pollMethod === 'votes' ? this.getBaseAmount(data) : this.getOptionBaseAmount(data, option);
|
|
||||||
}
|
|
||||||
if (!base || base < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const vote = option.votes.find(v => v.value === key);
|
|
||||||
if (!vote) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Math.round(((vote.weight * 100) / base) * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the percentage for a non-abstract per-poll value
|
|
||||||
* TODO: similar code to getPercent. Mergeable?
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param value a per-poll value (e.g. 'votesvalid')
|
|
||||||
* @returns a percentage number with two digits, null if the value cannot be calculated
|
|
||||||
*/
|
|
||||||
public getValuePercent(data: CalculationData, value: CalculablePollKey): number | null {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
switch (percentBase) {
|
|
||||||
case 'YES_NO':
|
|
||||||
case 'YES_NO_ABSTAIN':
|
|
||||||
case 'DISABLED':
|
|
||||||
return null;
|
|
||||||
case 'VALID':
|
|
||||||
if (value === 'votesinvalid' || value === 'votescast') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const baseAmount = this.getBaseAmount(data);
|
|
||||||
if (!baseAmount) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const amount = data[value];
|
|
||||||
if (amount === undefined || amount < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Math.round(((amount * 100) / baseAmount) * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the option in a poll is abstract (percentages should not be calculated)
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param option
|
|
||||||
* @param key (optional) the key to calculate
|
|
||||||
* @returns true if the poll has no percentages, the poll option is a special value,
|
|
||||||
* or if the calculations are disabled in the config
|
|
||||||
*/
|
|
||||||
public isAbstractOption(data: CalculationData, option: ViewAssignmentPollOption, key?: PollVoteValue): boolean {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
if (percentBase === 'DISABLED' || !option.votes || !option.votes.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key === 'Abstain' && percentBase === 'YES_NO') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (data.pollMethod === 'votes') {
|
|
||||||
return this.getBaseAmount(data) > 0 ? false : true;
|
|
||||||
} else {
|
|
||||||
return option.votes.some(v => v.weight < 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for abstract (not usable as percentage) options in non-option
|
|
||||||
* 'meta' values
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param value
|
|
||||||
* @returns true if percentages cannot be calculated
|
|
||||||
* TODO: Yes, No, etc. in an option will always return true.
|
|
||||||
* Use {@link isAbstractOption} for these
|
|
||||||
*/
|
|
||||||
public isAbstractValue(data: CalculationData, value: CalculablePollKey): boolean {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
if (percentBase === 'DISABLED' || !this.getBaseAmount(data) || !this.pollValues.includes(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (percentBase === 'CAST' && data[value] >= 0) {
|
|
||||||
return false;
|
|
||||||
} else if (percentBase === 'VALID' && value === 'votesvalid' && data[value] > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the base amount inside an option. Only useful if poll method is not 'votes'
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param option
|
|
||||||
* @returns an positive integer to be used as percentage base, or null
|
|
||||||
*/
|
|
||||||
private getOptionBaseAmount(data: CalculationData, option: CalculationOption): number | null {
|
|
||||||
const percentBase = data.percentBase || this.percentBase;
|
|
||||||
if (percentBase === 'DISABLED' || data.pollMethod === 'votes') {
|
|
||||||
return null;
|
|
||||||
} else if (percentBase === 'CAST') {
|
|
||||||
return data.votescast > 0 ? data.votescast : null;
|
|
||||||
} else if (percentBase === 'VALID') {
|
|
||||||
return data.votesvalid > 0 ? data.votesvalid : null;
|
|
||||||
}
|
|
||||||
const yes = option.votes.find(v => v.value === 'Yes');
|
|
||||||
const no = option.votes.find(v => v.value === 'No');
|
|
||||||
if (percentBase === 'YES_NO') {
|
|
||||||
if (!yes || yes.weight === undefined || !no || no.weight === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return yes.weight >= 0 && no.weight >= 0 ? yes.weight + no.weight : null;
|
|
||||||
} else if (percentBase === 'YES_NO_ABSTAIN') {
|
|
||||||
const abstain = option.votes.find(v => v.value === 'Abstain');
|
|
||||||
if (!abstain || abstain.weight === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return yes.weight >= 0 && no.weight >= 0 && abstain.weight >= 0
|
|
||||||
? yes.weight + no.weight + abstain.weight
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the minimum amount of votes needed for an option to pass the quorum
|
|
||||||
*
|
|
||||||
* @param method
|
|
||||||
* @param data
|
|
||||||
* @param option
|
|
||||||
* @returns a positive integer number; may return null if quorum is not calculable
|
|
||||||
*/
|
|
||||||
public yesQuorum(method: MajorityMethod, data: CalculationData, option: ViewAssignmentPollOption): number | null {
|
|
||||||
const baseAmount =
|
|
||||||
data.pollMethod === 'votes' ? this.getBaseAmount(data) : this.getOptionBaseAmount(data, option);
|
|
||||||
return method.calc(baseAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper function to tuirn a Poll into calculation data for this service
|
|
||||||
* TODO: temp until better method to normalize Poll ans PollSlideData is implemented
|
|
||||||
*
|
|
||||||
* @param poll
|
|
||||||
* @returns calculationData ready to be used
|
|
||||||
*/
|
|
||||||
public calculationDataFromPoll(poll: ViewAssignmentPoll): CalculationData {
|
|
||||||
return {
|
|
||||||
pollMethod: poll.pollmethod,
|
|
||||||
votesno: poll.votesno,
|
|
||||||
votesabstain: poll.votesabstain,
|
|
||||||
votescast: poll.votescast,
|
|
||||||
votesinvalid: poll.votesinvalid,
|
|
||||||
votesvalid: poll.votesvalid,
|
|
||||||
pollOptions: poll.options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,9 +5,7 @@ import { ViewConfig } from './models/view-config';
|
|||||||
|
|
||||||
export const ConfigAppConfig: AppConfig = {
|
export const ConfigAppConfig: AppConfig = {
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
models: [
|
models: [{ model: Config, viewModel: ViewConfig, repository: ConfigRepositoryService }],
|
||||||
{ collectionString: 'core/config', model: Config, viewModel: ViewConfig, repository: ConfigRepositoryService }
|
|
||||||
],
|
|
||||||
mainMenuEntries: [
|
mainMenuEntries: [
|
||||||
{
|
{
|
||||||
route: '/settings',
|
route: '/settings',
|
||||||
|
@ -7,7 +7,6 @@ export const MediafileAppConfig: AppConfig = {
|
|||||||
name: 'mediafiles',
|
name: 'mediafiles',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'mediafiles/mediafile',
|
|
||||||
model: Mediafile,
|
model: Mediafile,
|
||||||
viewModel: ViewMediafile,
|
viewModel: ViewMediafile,
|
||||||
searchOrder: 5,
|
searchOrder: 5,
|
||||||
|
12
client/src/app/site/motions/models/view-motion-option.ts
Normal file
12
client/src/app/site/motions/models/view-motion-option.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
||||||
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
|
||||||
|
export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
||||||
|
public get option(): MotionOption {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
|
||||||
|
protected _collectionString = MotionOption.COLLECTIONSTRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewMotionPoll extends MotionOption {}
|
41
client/src/app/site/motions/models/view-motion-poll.ts
Normal file
41
client/src/app/site/motions/models/view-motion-poll.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { MotionPoll, MotionPollWithoutNestedModels } from 'app/shared/models/motions/motion-poll';
|
||||||
|
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||||
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
|
||||||
|
export interface MotionPollTitleInformation {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewMotionPoll extends BaseProjectableViewModel<MotionPoll> implements MotionPollTitleInformation {
|
||||||
|
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
||||||
|
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
|
public get poll(): MotionPoll {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
|
/*return {
|
||||||
|
getBasicProjectorElement: options => ({
|
||||||
|
name: Motion.COLLECTIONSTRING,
|
||||||
|
id: this.id,
|
||||||
|
getIdentifiers: () => ['name', 'id']
|
||||||
|
}),
|
||||||
|
slideOptions: [],
|
||||||
|
projectionDefaultName: 'motions',
|
||||||
|
getDialogTitle: this.getTitle
|
||||||
|
};*/
|
||||||
|
throw new Error('TODO');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TIMotionPollRelations {
|
||||||
|
options: ViewMotionOption[];
|
||||||
|
voted: ViewUser[];
|
||||||
|
groups: ViewGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewMotionPoll extends MotionPollWithoutNestedModels, TIMotionPollRelations {}
|
17
client/src/app/site/motions/models/view-motion-vote.ts
Normal file
17
client/src/app/site/motions/models/view-motion-vote.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
|
||||||
|
export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
||||||
|
public get vote(): MotionVote {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
public static COLLECTIONSTRING = MotionVote.COLLECTIONSTRING;
|
||||||
|
protected _collectionString = MotionVote.COLLECTIONSTRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TIMotionVoteRelations {
|
||||||
|
user?: ViewUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewMotionVote extends MotionVote, TIMotionVoteRelations {}
|
@ -1382,7 +1382,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
* Handler for creating a poll
|
* Handler for creating a poll
|
||||||
*/
|
*/
|
||||||
public createPoll(): void {
|
public createPoll(): void {
|
||||||
this.repo.createPoll(this.motion).catch(this.raiseError);
|
// TODO
|
||||||
|
// this.repo.createPoll(<any>{}).catch(this.raiseError);
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
@ -35,8 +32,6 @@ export class MotionPollDialogComponent {
|
|||||||
public constructor(
|
public constructor(
|
||||||
public dialogRef: MatDialogRef<MotionPollDialogComponent>,
|
public dialogRef: MatDialogRef<MotionPollDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: MotionPoll,
|
@Inject(MAT_DIALOG_DATA) public data: MotionPoll,
|
||||||
private matSnackBar: MatSnackBar,
|
|
||||||
private translate: TranslateService,
|
|
||||||
private pollService: MotionPollService
|
private pollService: MotionPollService
|
||||||
) {
|
) {
|
||||||
this.pollKeys = this.pollService.pollValues;
|
this.pollKeys = this.pollService.pollValues;
|
||||||
@ -57,7 +52,7 @@ export class MotionPollDialogComponent {
|
|||||||
* TODO better validation
|
* TODO better validation
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
if (this.data.yes === undefined || this.data.no === undefined || this.data.abstain === undefined) {
|
/*if (this.data.yes === undefined || this.data.no === undefined || this.data.abstain === undefined) {
|
||||||
this.matSnackBar.open(
|
this.matSnackBar.open(
|
||||||
this.translate.instant('Please fill in all required values'),
|
this.translate.instant('Please fill in all required values'),
|
||||||
this.translate.instant('OK'),
|
this.translate.instant('OK'),
|
||||||
@ -67,7 +62,7 @@ export class MotionPollDialogComponent {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.dialogRef.close(this.data);
|
this.dialogRef.close(this.data);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container class="meta-text-block-content">
|
<ng-container class="meta-text-block-content">
|
||||||
<div class="motion-poll-wrapper">
|
<div class="motion-poll-wrapper">
|
||||||
<div *ngIf="poll.has_votes" class="poll-result">
|
<!--<div *ngIf="poll.has_votes" class="poll-result">--><div>
|
||||||
<div *ngFor="let key of pollValues">
|
<div *ngFor="let key of pollValues">
|
||||||
<div class="poll-progress" *ngIf="poll[key] !== undefined">
|
<div class="poll-progress" *ngIf="poll[key] !== undefined">
|
||||||
<mat-icon class="main-nav-color" matTooltip="{{ getLabel(key) | translate }}">
|
<mat-icon class="main-nav-color" matTooltip="{{ getLabel(key) | translate }}">
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
|
||||||
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
|
||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
||||||
import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service';
|
import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service';
|
||||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { MotionPollDialogComponent } from './motion-poll-dialog.component';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component used to display and edit polls of a motion.
|
* A component used to display and edit polls of a motion.
|
||||||
@ -97,9 +92,7 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
public pollService: MotionPollService,
|
public pollService: MotionPollService,
|
||||||
private motionRepo: MotionRepositoryService,
|
|
||||||
private constants: ConstantsService,
|
private constants: ConstantsService,
|
||||||
private promptService: PromptService,
|
|
||||||
public perms: LocalPermissionsService,
|
public perms: LocalPermissionsService,
|
||||||
private pdfService: MotionPollPdfService
|
private pdfService: MotionPollPdfService
|
||||||
) {
|
) {
|
||||||
@ -113,7 +106,7 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
* Subscribes to updates of itself
|
* Subscribes to updates of itself
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.poll = new MotionPoll(this.rawPoll);
|
/*this.poll = new MotionPoll(this.rawPoll);
|
||||||
this.motionRepo.getViewModelObservable(this.poll.motion_id).subscribe(viewmotion => {
|
this.motionRepo.getViewModelObservable(this.poll.motion_id).subscribe(viewmotion => {
|
||||||
if (viewmotion) {
|
if (viewmotion) {
|
||||||
const updatePoll = viewmotion.motion.polls.find(poll => poll.id === this.poll.id);
|
const updatePoll = viewmotion.motion.polls.find(poll => poll.id === this.poll.id);
|
||||||
@ -121,17 +114,18 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.poll = new MotionPoll(updatePoll);
|
this.poll = new MotionPoll(updatePoll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a delete request for this poll after a confirmation dialog has been accepted.
|
* Sends a delete request for this poll after a confirmation dialog has been accepted.
|
||||||
*/
|
*/
|
||||||
public async deletePoll(): Promise<void> {
|
public async deletePoll(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this vote?');
|
/*const title = this.translate.instant('Are you sure you want to delete this vote?');
|
||||||
if (await this.promptService.open(title)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.motionRepo.deletePoll(this.poll).catch(this.raiseError);
|
this.motionRepo.deletePoll(this.poll).catch(this.raiseError);
|
||||||
}
|
}*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,7 +186,7 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
* Triggers the 'edit poll' dialog'
|
* Triggers the 'edit poll' dialog'
|
||||||
*/
|
*/
|
||||||
public editPoll(): void {
|
public editPoll(): void {
|
||||||
const dialogRef = this.dialog.open(MotionPollDialogComponent, {
|
/*const dialogRef = this.dialog.open(MotionPollDialogComponent, {
|
||||||
data: { ...this.poll },
|
data: { ...this.poll },
|
||||||
...infoDialogSettings
|
...infoDialogSettings
|
||||||
});
|
});
|
||||||
@ -200,7 +194,8 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
if (result) {
|
if (result) {
|
||||||
this.motionRepo.updatePoll(result).catch(this.raiseError);
|
this.motionRepo.updatePoll(result).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,7 +204,8 @@ export class MotionPollComponent extends BaseViewComponent implements OnInit {
|
|||||||
* @returns true if the quorum is reached
|
* @returns true if the quorum is reached
|
||||||
*/
|
*/
|
||||||
public get quorumYesReached(): boolean {
|
public get quorumYesReached(): boolean {
|
||||||
return this.poll.yes >= this.yesQuorum;
|
// return this.poll.yes >= this.yesQuorum;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,52 +28,44 @@ export const MotionsAppConfig: AppConfig = {
|
|||||||
name: 'motions',
|
name: 'motions',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'motions/motion',
|
|
||||||
model: Motion,
|
model: Motion,
|
||||||
viewModel: ViewMotion,
|
viewModel: ViewMotion,
|
||||||
searchOrder: 2,
|
searchOrder: 2,
|
||||||
repository: MotionRepositoryService
|
repository: MotionRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/category',
|
|
||||||
model: Category,
|
model: Category,
|
||||||
viewModel: ViewCategory,
|
viewModel: ViewCategory,
|
||||||
searchOrder: 6,
|
searchOrder: 6,
|
||||||
repository: CategoryRepositoryService
|
repository: CategoryRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/workflow',
|
|
||||||
model: Workflow,
|
model: Workflow,
|
||||||
viewModel: ViewWorkflow,
|
viewModel: ViewWorkflow,
|
||||||
repository: WorkflowRepositoryService
|
repository: WorkflowRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/state',
|
|
||||||
model: State,
|
model: State,
|
||||||
viewModel: ViewState,
|
viewModel: ViewState,
|
||||||
repository: StateRepositoryService
|
repository: StateRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/motion-comment-section',
|
|
||||||
model: MotionCommentSection,
|
model: MotionCommentSection,
|
||||||
viewModel: ViewMotionCommentSection,
|
viewModel: ViewMotionCommentSection,
|
||||||
repository: MotionCommentSectionRepositoryService
|
repository: MotionCommentSectionRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/motion-change-recommendation',
|
|
||||||
model: MotionChangeRecommendation,
|
model: MotionChangeRecommendation,
|
||||||
viewModel: ViewMotionChangeRecommendation,
|
viewModel: ViewMotionChangeRecommendation,
|
||||||
repository: ChangeRecommendationRepositoryService
|
repository: ChangeRecommendationRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/motion-block',
|
|
||||||
model: MotionBlock,
|
model: MotionBlock,
|
||||||
viewModel: ViewMotionBlock,
|
viewModel: ViewMotionBlock,
|
||||||
searchOrder: 7,
|
searchOrder: 7,
|
||||||
repository: MotionBlockRepositoryService
|
repository: MotionBlockRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'motions/statute-paragraph',
|
|
||||||
model: StatuteParagraph,
|
model: StatuteParagraph,
|
||||||
viewModel: ViewStatuteParagraph,
|
viewModel: ViewStatuteParagraph,
|
||||||
searchOrder: 9,
|
searchOrder: 9,
|
||||||
|
@ -10,11 +10,9 @@ import { MotionRepositoryService } from 'app/core/repositories/motions/motion-re
|
|||||||
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
|
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||||
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
|
||||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
||||||
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
|
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
|
||||||
import { MotionExportInfo } from './motion-export.service';
|
import { MotionExportInfo } from './motion-export.service';
|
||||||
import { MotionPollService } from './motion-poll.service';
|
|
||||||
import { ChangeRecoMode, InfoToExport, LineNumberingMode, PERSONAL_NOTE_ID } from '../motions.constants';
|
import { ChangeRecoMode, InfoToExport, LineNumberingMode, PERSONAL_NOTE_ID } from '../motions.constants';
|
||||||
import { ViewMotion } from '../models/view-motion';
|
import { ViewMotion } from '../models/view-motion';
|
||||||
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
||||||
@ -62,7 +60,6 @@ export class MotionPdfService {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private pdfDocumentService: PdfDocumentService,
|
private pdfDocumentService: PdfDocumentService,
|
||||||
private htmlToPdfService: HtmlToPdfService,
|
private htmlToPdfService: HtmlToPdfService,
|
||||||
private pollService: MotionPollService,
|
|
||||||
private linenumberingService: LinenumberingService,
|
private linenumberingService: LinenumberingService,
|
||||||
private commentRepo: MotionCommentSectionRepositoryService
|
private commentRepo: MotionCommentSectionRepositoryService
|
||||||
) {}
|
) {}
|
||||||
@ -366,7 +363,7 @@ export class MotionPdfService {
|
|||||||
const column2 = [];
|
const column2 = [];
|
||||||
const column3 = [];
|
const column3 = [];
|
||||||
motion.motion.polls.map((poll, index) => {
|
motion.motion.polls.map((poll, index) => {
|
||||||
if (poll.has_votes) {
|
/*if (poll.has_votes) {
|
||||||
if (motion.motion.polls.length > 1) {
|
if (motion.motion.polls.length > 1) {
|
||||||
column1.push(index + 1 + '. ' + this.translate.instant('Vote'));
|
column1.push(index + 1 + '. ' + this.translate.instant('Vote'));
|
||||||
column2.push('');
|
column2.push('');
|
||||||
@ -389,7 +386,7 @@ export class MotionPdfService {
|
|||||||
? column3.push('')
|
? column3.push('')
|
||||||
: column3.push(`(${this.pollService.calculatePercentage(poll, value)} %)`);
|
: column3.push(`(${this.pollService.calculatePercentage(poll, value)} %)`);
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
});
|
});
|
||||||
metaTableBody.push([
|
metaTableBody.push([
|
||||||
{
|
{
|
||||||
|
@ -73,7 +73,7 @@ export class MotionPollService extends PollService {
|
|||||||
* the base cannot be calculated
|
* the base cannot be calculated
|
||||||
*/
|
*/
|
||||||
public getBaseAmount(poll: MotionPoll): number {
|
public getBaseAmount(poll: MotionPoll): number {
|
||||||
if (!poll) {
|
/*if (!poll) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
switch (this.percentBase) {
|
switch (this.percentBase) {
|
||||||
@ -102,7 +102,8 @@ export class MotionPollService extends PollService {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return poll.yes + poll.no;
|
return poll.yes + poll.no;
|
||||||
}
|
}*/
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,25 +16,21 @@ export const ProjectorAppConfig: AppConfig = {
|
|||||||
name: 'projector',
|
name: 'projector',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'core/projector',
|
|
||||||
model: Projector,
|
model: Projector,
|
||||||
viewModel: ViewProjector,
|
viewModel: ViewProjector,
|
||||||
repository: ProjectorRepositoryService
|
repository: ProjectorRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'core/projection-default',
|
|
||||||
model: ProjectionDefault,
|
model: ProjectionDefault,
|
||||||
viewModel: ViewProjectionDefault,
|
viewModel: ViewProjectionDefault,
|
||||||
repository: ProjectionDefaultRepositoryService
|
repository: ProjectionDefaultRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'core/countdown',
|
|
||||||
model: Countdown,
|
model: Countdown,
|
||||||
viewModel: ViewCountdown,
|
viewModel: ViewCountdown,
|
||||||
repository: CountdownRepositoryService
|
repository: CountdownRepositoryService
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collectionString: 'core/projector-message',
|
|
||||||
model: ProjectorMessage,
|
model: ProjectorMessage,
|
||||||
viewModel: ViewProjectorMessage,
|
viewModel: ViewProjectorMessage,
|
||||||
repository: ProjectorMessageRepositoryService
|
repository: ProjectorMessageRepositoryService
|
||||||
|
@ -7,7 +7,6 @@ export const TagAppConfig: AppConfig = {
|
|||||||
name: 'tag',
|
name: 'tag',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'core/tag',
|
|
||||||
model: Tag,
|
model: Tag,
|
||||||
viewModel: ViewTag,
|
viewModel: ViewTag,
|
||||||
searchOrder: 8,
|
searchOrder: 8,
|
||||||
|
@ -7,7 +7,6 @@ export const TopicsAppConfig: AppConfig = {
|
|||||||
name: 'topics',
|
name: 'topics',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'topics/topic',
|
|
||||||
model: Topic,
|
model: Topic,
|
||||||
viewModel: ViewTopic,
|
viewModel: ViewTopic,
|
||||||
searchOrder: 1,
|
searchOrder: 1,
|
||||||
|
@ -13,15 +13,13 @@ export const UsersAppConfig: AppConfig = {
|
|||||||
name: 'users',
|
name: 'users',
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
collectionString: 'users/user',
|
|
||||||
model: User,
|
model: User,
|
||||||
viewModel: ViewUser,
|
viewModel: ViewUser,
|
||||||
searchOrder: 4,
|
searchOrder: 4,
|
||||||
repository: UserRepositoryService
|
repository: UserRepositoryService
|
||||||
},
|
},
|
||||||
{ collectionString: 'users/group', model: Group, viewModel: ViewGroup, repository: GroupRepositoryService },
|
{ model: Group, viewModel: ViewGroup, repository: GroupRepositoryService },
|
||||||
{
|
{
|
||||||
collectionString: 'users/personal-note',
|
|
||||||
model: PersonalNote,
|
model: PersonalNote,
|
||||||
viewModel: ViewPersonalNote,
|
viewModel: ViewPersonalNote,
|
||||||
repository: PersonalNoteRepositoryService
|
repository: PersonalNoteRepositoryService
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||||
import { AssignmentPercentBase, AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service';
|
|
||||||
|
|
||||||
export interface PollSlideOption {
|
export interface PollSlideOption {
|
||||||
user: string;
|
user: string;
|
||||||
@ -12,12 +11,12 @@ export interface PollSlideOption {
|
|||||||
|
|
||||||
export interface PollSlideData {
|
export interface PollSlideData {
|
||||||
title: string;
|
title: string;
|
||||||
assignments_poll_100_percent_base: AssignmentPercentBase;
|
assignments_poll_100_percent_base: any /*AssignmentPercentBase*/;
|
||||||
poll: {
|
poll: {
|
||||||
published: boolean;
|
published: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
has_votes?: boolean;
|
has_votes?: boolean;
|
||||||
pollmethod?: AssignmentPollMethod;
|
pollmethod?: any /*AssignmentPollmethods*/;
|
||||||
votesno?: string;
|
votesno?: string;
|
||||||
votesabstain?: string;
|
votesabstain?: string;
|
||||||
votesvalid?: string;
|
votesvalid?: string;
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { SlideData } from 'app/core/core-services/projector-data.service';
|
import { SlideData } from 'app/core/core-services/projector-data.service';
|
||||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||||
import {
|
|
||||||
AssignmentPollService,
|
|
||||||
CalculationData,
|
|
||||||
SummaryPollKey
|
|
||||||
} from 'app/site/assignments/services/assignment-poll.service';
|
|
||||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||||
import { PollSlideData, PollSlideOption } from './poll-slide-data';
|
import { PollSlideData, PollSlideOption } from './poll-slide-data';
|
||||||
|
|
||||||
@ -20,20 +13,19 @@ import { PollSlideData, PollSlideOption } from './poll-slide-data';
|
|||||||
export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
||||||
private _data: SlideData<PollSlideData>;
|
private _data: SlideData<PollSlideData>;
|
||||||
|
|
||||||
private calculationData: CalculationData;
|
public get pollValues(): any {
|
||||||
|
// SummaryPollKey[] {
|
||||||
public get pollValues(): SummaryPollKey[] {
|
|
||||||
if (!this.data) {
|
if (!this.data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const values: SummaryPollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
|
const values: any /*SummaryPollKey[]*/ = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
|
||||||
return values.filter(val => this.data.data.poll[val] !== null);
|
return values.filter(val => this.data.data.poll[val] !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public set data(data: SlideData<PollSlideData>) {
|
public set data(data: SlideData<PollSlideData>) {
|
||||||
this._data = data;
|
this._data = data;
|
||||||
this.calculationData = {
|
/*this.calculationData = {
|
||||||
pollMethod: data.data.poll.pollmethod,
|
pollMethod: data.data.poll.pollmethod,
|
||||||
votesno: parseFloat(data.data.poll.votesno),
|
votesno: parseFloat(data.data.poll.votesno),
|
||||||
votesabstain: parseFloat(data.data.poll.votesabstain),
|
votesabstain: parseFloat(data.data.poll.votesabstain),
|
||||||
@ -51,17 +43,13 @@ export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
percentBase: data.data.assignments_poll_100_percent_base
|
percentBase: data.data.assignments_poll_100_percent_base
|
||||||
};
|
};*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public get data(): SlideData<PollSlideData> {
|
public get data(): SlideData<PollSlideData> {
|
||||||
return this._data;
|
return this._data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(private pollService: AssignmentPollService, private translate: TranslateService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get a vote's numerical or special label, including percent values if these are to
|
* get a vote's numerical or special label, including percent values if these are to
|
||||||
* be displayed
|
* be displayed
|
||||||
@ -70,7 +58,7 @@ export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
|||||||
* @param option
|
* @param option
|
||||||
*/
|
*/
|
||||||
public getVotePercent(key: PollVoteValue, option: PollSlideOption): string {
|
public getVotePercent(key: PollVoteValue, option: PollSlideOption): string {
|
||||||
const calcOption = {
|
/*const calcOption = {
|
||||||
votes: option.votes.map(vote => {
|
votes: option.votes.map(vote => {
|
||||||
return { weight: parseFloat(vote.weight), value: vote.value };
|
return { weight: parseFloat(vote.weight), value: vote.value };
|
||||||
})
|
})
|
||||||
@ -79,19 +67,22 @@ export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
|||||||
const number = this.translate.instant(
|
const number = this.translate.instant(
|
||||||
this.pollService.getSpecialLabel(parseFloat(option.votes.find(v => v.value === key).weight))
|
this.pollService.getSpecialLabel(parseFloat(option.votes.find(v => v.value === key).weight))
|
||||||
);
|
);
|
||||||
return percent === null ? number : `${number} (${percent}%)`;
|
return percent === null ? number : `${number} (${percent}%)`;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPollPercent(key: CalculablePollKey): string {
|
public getPollPercent(key: CalculablePollKey): string {
|
||||||
const percent = this.pollService.getValuePercent(this.calculationData, key);
|
/*const percent = this.pollService.getValuePercent(this.calculationData, key);
|
||||||
const number = this.translate.instant(this.pollService.getSpecialLabel(this.calculationData[key]));
|
const number = this.translate.instant(this.pollService.getSpecialLabel(this.calculationData[key]));
|
||||||
return percent === null ? number : `${number} (${percent}%)`;
|
return percent === null ? number : `${number} (${percent}%)`;*/
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a translated label for a key
|
* @returns a translated label for a key
|
||||||
*/
|
*/
|
||||||
public getLabel(key: CalculablePollKey): string {
|
public getLabel(key: CalculablePollKey): string {
|
||||||
return this.translate.instant(this.pollService.getLabel(key));
|
// return this.translate.instant(this.pollService.getLabel(key));
|
||||||
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,16 +21,22 @@ class AgendaItemMixin(models.Model):
|
|||||||
class Meta(Unsafe):
|
class Meta(Unsafe):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
"""
|
|
||||||
Container for runtime information for agenda app (on create or update of this instance).
|
|
||||||
Can be an attribute of an item, e.g. "type", "parent_id", "comment", "duration", "weight",
|
|
||||||
or "create", which determinates, if the items should be created. If not given, the
|
|
||||||
config value is used.
|
|
||||||
"""
|
|
||||||
agenda_item_update_information: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
agenda_item_skip_autoupdate = False
|
agenda_item_skip_autoupdate = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.agenda_item_update_information: Dict[str, Any] = {}
|
||||||
|
"""
|
||||||
|
Container for runtime information for agenda app (on create or update of this instance).
|
||||||
|
Can be an attribute of an item, e.g. "type", "parent_id", "comment", "duration", "weight",
|
||||||
|
or "create", which determinates, if the items should be created. If not given, the
|
||||||
|
config value is used.
|
||||||
|
|
||||||
|
Important: Do not just write this into the class definition, becuase the object would become
|
||||||
|
shared within all instances inherited from this class!
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agenda_item(self):
|
def agenda_item(self):
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,6 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if is_agenda_item_content_object:
|
if is_agenda_item_content_object:
|
||||||
if created:
|
if created:
|
||||||
|
|
||||||
if instance.get_collection_string() == "topics/topic":
|
if instance.get_collection_string() == "topics/topic":
|
||||||
should_create_item = True
|
should_create_item = True
|
||||||
elif config["agenda_item_creation"] == "always":
|
elif config["agenda_item_creation"] == "always":
|
||||||
|
@ -42,3 +42,12 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
data = []
|
data = []
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentPollAccessPermissions(BaseAccessPermissions):
|
||||||
|
base_permission = "assignments.can_see"
|
||||||
|
|
||||||
|
async def get_restricted_data(
|
||||||
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
return full_data
|
||||||
|
@ -15,7 +15,11 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from .projector import register_projector_slides
|
from .projector import register_projector_slides
|
||||||
from .signals import get_permission_change_data
|
from .signals import get_permission_change_data
|
||||||
from .views import AssignmentViewSet, AssignmentPollViewSet
|
from .views import (
|
||||||
|
AssignmentViewSet,
|
||||||
|
AssignmentPollViewSet,
|
||||||
|
AssignmentVoteViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
# Define projector elements.
|
# Define projector elements.
|
||||||
register_projector_slides()
|
register_projector_slides()
|
||||||
@ -30,7 +34,14 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
router.register(
|
router.register(
|
||||||
self.get_model("Assignment").get_collection_string(), AssignmentViewSet
|
self.get_model("Assignment").get_collection_string(), AssignmentViewSet
|
||||||
)
|
)
|
||||||
router.register("assignments/poll", AssignmentPollViewSet)
|
router.register(
|
||||||
|
self.get_model("AssignmentPoll").get_collection_string(),
|
||||||
|
AssignmentPollViewSet,
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("AssignmentVote").get_collection_string(),
|
||||||
|
AssignmentVoteViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
# Register required_users
|
# Register required_users
|
||||||
required_user.add_collection_string(
|
required_user.add_collection_string(
|
||||||
@ -47,13 +58,16 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
yield self.get_model("Assignment")
|
for model_name in ("Assignment", "AssignmentPoll", "AssignmentVote"):
|
||||||
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
|
||||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
Returns all user ids that are displayed as candidates (including poll
|
Returns all user ids that are displayed as candidates (including poll
|
||||||
options) in the assignment element.
|
options) in the assignment element.
|
||||||
|
|
||||||
|
TODO: Adapt this method for new poll structure!!
|
||||||
"""
|
"""
|
||||||
candidates = set(
|
candidates = set(
|
||||||
related_user["user_id"] for related_user in element["assignment_related_users"]
|
related_user["user_id"] for related_user in element["assignment_related_users"]
|
||||||
|
@ -12,25 +12,6 @@ def get_config_variables():
|
|||||||
to be evaluated during app loading (see apps.py).
|
to be evaluated during app loading (see apps.py).
|
||||||
"""
|
"""
|
||||||
# Ballot and ballot papers
|
# Ballot and ballot papers
|
||||||
yield ConfigVariable(
|
|
||||||
name="assignments_poll_vote_values",
|
|
||||||
default_value="auto",
|
|
||||||
input_type="choice",
|
|
||||||
label="Election method",
|
|
||||||
choices=(
|
|
||||||
{"value": "auto", "display_name": "Automatic assign of method"},
|
|
||||||
{"value": "votes", "display_name": "Always one option per candidate"},
|
|
||||||
{
|
|
||||||
"value": "yesnoabstain",
|
|
||||||
"display_name": "Always Yes-No-Abstain per candidate",
|
|
||||||
},
|
|
||||||
{"value": "yesno", "display_name": "Always Yes/No per candidate"},
|
|
||||||
),
|
|
||||||
weight=410,
|
|
||||||
group="Elections",
|
|
||||||
subgroup="Ballot and ballot papers",
|
|
||||||
)
|
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="assignments_poll_100_percent_base",
|
name="assignments_poll_100_percent_base",
|
||||||
default_value="YES_NO_ABSTAIN",
|
default_value="YES_NO_ABSTAIN",
|
||||||
|
139
openslides/assignments/migrations/0008_auto_20191017_1040.py
Normal file
139
openslides/assignments/migrations/0008_auto_20191017_1040.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-17 08:40
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0011_postgresql_auth_group_id_sequence"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("assignments", "0007_assignment_attachments"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentoption", old_name="candidate", new_name="user"
|
||||||
|
),
|
||||||
|
migrations.RemoveField(model_name="assignmentpoll", name="description"),
|
||||||
|
migrations.RemoveField(model_name="assignmentpoll", name="published"),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="global_abstain",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="global_no",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(blank=True, to="users.Group"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="state",
|
||||||
|
field=models.IntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Created"),
|
||||||
|
(2, "Started"),
|
||||||
|
(3, "Finished"),
|
||||||
|
(4, "Published"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(default="Poll", max_length=255, blank=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("analog", "Analog"),
|
||||||
|
("named", "Named"),
|
||||||
|
("pseudoanonymous", "Pseudoanonymous"),
|
||||||
|
],
|
||||||
|
default="analog",
|
||||||
|
max_length=64,
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="votes_amount",
|
||||||
|
field=models.IntegerField(
|
||||||
|
default=1, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="voted",
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="allow_multiple_votes_per_candidate",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="pollmethod",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("YN", "YN"), ("YNA", "YNA"), ("votes", "votes")], max_length=5
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="value",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("Y", "Y"), ("N", "N"), ("A", "A")], max_length=1
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="weight",
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=6,
|
||||||
|
default=Decimal("1"),
|
||||||
|
max_digits=15,
|
||||||
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll", old_name="votescast", new_name="db_votescast"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
old_name="votesinvalid",
|
||||||
|
new_name="db_votesinvalid",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll", old_name="votesvalid", new_name="db_votesvalid"
|
||||||
|
),
|
||||||
|
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
|
||||||
|
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
|
||||||
|
]
|
@ -1,5 +1,4 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,19 +10,16 @@ from openslides.agenda.models import Speaker
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.poll.models import (
|
from openslides.poll.models import BaseOption, BasePoll, BaseVote
|
||||||
BaseOption,
|
|
||||||
BasePoll,
|
|
||||||
BaseVote,
|
|
||||||
CollectDefaultVotesMixin,
|
|
||||||
PublishPollMixin,
|
|
||||||
)
|
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
from .access_permissions import (
|
||||||
|
AssignmentAccessPermissions,
|
||||||
|
AssignmentPollAccessPermissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||||
@ -196,7 +192,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
"""
|
"""
|
||||||
return self.elected.filter(pk=user.pk).exists()
|
return self.elected.filter(pk=user.pk).exists()
|
||||||
|
|
||||||
def set_candidate(self, user):
|
def add_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
Adds the user as candidate.
|
Adds the user as candidate.
|
||||||
"""
|
"""
|
||||||
@ -215,7 +211,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
user=user, defaults={"elected": True}
|
user=user, defaults={"elected": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_related_user(self, user):
|
def remove_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
Delete the connection from the assignment to the user.
|
Delete the connection from the assignment to the user.
|
||||||
"""
|
"""
|
||||||
@ -233,59 +229,6 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
|
|
||||||
self.phase = phase
|
self.phase = phase
|
||||||
|
|
||||||
def create_poll(self):
|
|
||||||
"""
|
|
||||||
Creates a new poll for the assignment and adds all candidates to all
|
|
||||||
lists of speakers of related agenda items.
|
|
||||||
"""
|
|
||||||
candidates = self.candidates.all()
|
|
||||||
|
|
||||||
# Find out the method of the election
|
|
||||||
if config["assignments_poll_vote_values"] == "votes":
|
|
||||||
pollmethod = "votes"
|
|
||||||
elif config["assignments_poll_vote_values"] == "yesnoabstain":
|
|
||||||
pollmethod = "yna"
|
|
||||||
elif config["assignments_poll_vote_values"] == "yesno":
|
|
||||||
pollmethod = "yn"
|
|
||||||
else:
|
|
||||||
# config['assignments_poll_vote_values'] == 'auto'
|
|
||||||
# candidates <= available posts -> yes/no/abstain
|
|
||||||
if len(candidates) <= (self.open_posts - self.elected.count()):
|
|
||||||
pollmethod = "yna"
|
|
||||||
else:
|
|
||||||
pollmethod = "votes"
|
|
||||||
|
|
||||||
# Create the poll with the candidates.
|
|
||||||
poll = self.polls.create(
|
|
||||||
description=self.poll_description_default, pollmethod=pollmethod
|
|
||||||
)
|
|
||||||
options = []
|
|
||||||
related_users = AssignmentRelatedUser.objects.filter(
|
|
||||||
assignment__id=self.id
|
|
||||||
).exclude(elected=True)
|
|
||||||
for related_user in related_users:
|
|
||||||
options.append(
|
|
||||||
{"candidate": related_user.user, "weight": related_user.weight}
|
|
||||||
)
|
|
||||||
poll.set_options(options, skip_autoupdate=True)
|
|
||||||
inform_changed_data(self)
|
|
||||||
|
|
||||||
# Add all candidates to list of speakers of related agenda item
|
|
||||||
# TODO: Try to do this in a bulk create
|
|
||||||
if config["assignments_add_candidates_to_list_of_speakers"]:
|
|
||||||
for candidate in self.candidates:
|
|
||||||
try:
|
|
||||||
Speaker.objects.add(
|
|
||||||
candidate, self.list_of_speakers, skip_autoupdate=True
|
|
||||||
)
|
|
||||||
except OpenSlidesError:
|
|
||||||
# The Speaker is already on the list. Do nothing.
|
|
||||||
# TODO: Find a smart way not to catch the error concerning AnonymousUser.
|
|
||||||
pass
|
|
||||||
inform_changed_data(self.list_of_speakers)
|
|
||||||
|
|
||||||
return poll
|
|
||||||
|
|
||||||
def vote_results(self, only_published):
|
def vote_results(self, only_published):
|
||||||
"""
|
"""
|
||||||
Returns a table represented as a list with all candidates from all
|
Returns a table represented as a list with all candidates from all
|
||||||
@ -332,88 +275,76 @@ class AssignmentVote(RESTModelMixin, BaseVote):
|
|||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
|
||||||
"""
|
|
||||||
Returns the assignment to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.option.poll.assignment
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentOption(RESTModelMixin, BaseOption):
|
class AssignmentOption(RESTModelMixin, BaseOption):
|
||||||
|
vote_class = AssignmentVote
|
||||||
|
|
||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
||||||
)
|
)
|
||||||
candidate = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
weight = models.IntegerField(default=0)
|
weight = models.IntegerField(default=0)
|
||||||
|
|
||||||
vote_class = AssignmentVote
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.candidate)
|
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
return self.poll
|
||||||
Returns the assignment to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.poll.assignment
|
|
||||||
|
|
||||||
|
|
||||||
|
# Meta-TODO: Is this todo resolved?
|
||||||
# TODO: remove the type-ignoring in the next line, after this is solved:
|
# TODO: remove the type-ignoring in the next line, after this is solved:
|
||||||
# https://github.com/python/mypy/issues/3855
|
# https://github.com/python/mypy/issues/3855
|
||||||
class AssignmentPoll( # type: ignore
|
class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||||
RESTModelMixin, CollectDefaultVotesMixin, PublishPollMixin, BasePoll
|
access_permissions = AssignmentPollAccessPermissions()
|
||||||
):
|
|
||||||
option_class = AssignmentOption
|
option_class = AssignmentOption
|
||||||
|
|
||||||
assignment = models.ForeignKey(
|
assignment = models.ForeignKey(
|
||||||
Assignment, on_delete=models.CASCADE, related_name="polls"
|
Assignment, on_delete=models.CASCADE, related_name="polls"
|
||||||
)
|
)
|
||||||
pollmethod = models.CharField(max_length=5, default="yna")
|
|
||||||
description = models.CharField(max_length=79, blank=True)
|
|
||||||
|
|
||||||
votesabstain = models.DecimalField(
|
POLLMETHOD_YN = "YN"
|
||||||
null=True,
|
POLLMETHOD_YNA = "YNA"
|
||||||
blank=True,
|
POLLMETHOD_VOTES = "votes"
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
POLLMETHODS = (("YN", "YN"), ("YNA", "YNA"), ("votes", "votes"))
|
||||||
max_digits=15,
|
pollmethod = models.CharField(max_length=5, choices=POLLMETHODS)
|
||||||
decimal_places=6,
|
|
||||||
)
|
global_abstain = models.BooleanField(default=True)
|
||||||
""" General abstain votes, used for pollmethod 'votes' """
|
global_no = models.BooleanField(default=True)
|
||||||
votesno = models.DecimalField(
|
|
||||||
null=True,
|
votes_amount = models.IntegerField(default=1, validators=[MinValueValidator(1)])
|
||||||
blank=True,
|
""" For "votes" mode: The amount of votes a voter can give. """
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
|
||||||
max_digits=15,
|
allow_multiple_votes_per_candidate = models.BooleanField(default=False)
|
||||||
decimal_places=6,
|
|
||||||
)
|
|
||||||
""" General no votes, used for pollmethod 'votes' """
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def get_assignment(self):
|
def create_options(self):
|
||||||
return self.assignment
|
related_users = AssignmentRelatedUser.objects.filter(
|
||||||
|
assignment__id=self.assignment.id
|
||||||
|
).exclude(elected=True)
|
||||||
|
options = [
|
||||||
|
AssignmentOption(
|
||||||
|
user=related_user.user, weight=related_user.weight, poll=self
|
||||||
|
)
|
||||||
|
for related_user in related_users
|
||||||
|
]
|
||||||
|
AssignmentOption.objects.bulk_create(options)
|
||||||
|
inform_changed_data(self)
|
||||||
|
|
||||||
def get_vote_values(self):
|
# Add all candidates to list of speakers of related agenda item
|
||||||
if self.pollmethod == "yna":
|
if config["assignments_add_candidates_to_list_of_speakers"]:
|
||||||
return ["Yes", "No", "Abstain"]
|
for related_user in related_users:
|
||||||
if self.pollmethod == "yn":
|
try:
|
||||||
return ["Yes", "No"]
|
Speaker.objects.add(
|
||||||
return ["Votes"]
|
related_user.user,
|
||||||
|
self.assignment.list_of_speakers,
|
||||||
def get_ballot(self):
|
skip_autoupdate=True,
|
||||||
return self.assignment.polls.filter(id__lte=self.pk).count()
|
)
|
||||||
|
except OpenSlidesError:
|
||||||
def get_percent_base_choice(self):
|
# The Speaker is already on the list. Do nothing.
|
||||||
return config["assignments_poll_100_percent_base"]
|
pass
|
||||||
|
inform_changed_data(self.assignment.list_of_speakers)
|
||||||
def get_root_rest_element(self):
|
|
||||||
"""
|
|
||||||
Returns the assignment to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.assignment
|
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
from django.db import transaction
|
from openslides.poll.serializers import (
|
||||||
|
BASE_OPTION_FIELDS,
|
||||||
from openslides.poll.serializers import default_votes_validator
|
BASE_POLL_FIELDS,
|
||||||
|
BASE_VOTE_FIELDS,
|
||||||
|
)
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
|
CharField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
DictField,
|
IdPrimaryKeyRelatedField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
ListField,
|
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import get_group_model, has_perm
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.validate import validate_html
|
from ..utils.validate import validate_html
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -42,13 +44,7 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentRelatedUser
|
model = AssignmentRelatedUser
|
||||||
fields = (
|
fields = ("id", "user", "elected", "weight")
|
||||||
"id",
|
|
||||||
"user",
|
|
||||||
"elected",
|
|
||||||
"assignment",
|
|
||||||
"weight",
|
|
||||||
) # js-data needs the assignment-id in the nested object to define relations.
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentVoteSerializer(ModelSerializer):
|
class AssignmentVoteSerializer(ModelSerializer):
|
||||||
@ -56,9 +52,15 @@ class AssignmentVoteSerializer(ModelSerializer):
|
|||||||
Serializer for assignment.models.AssignmentVote objects.
|
Serializer for assignment.models.AssignmentVote objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pollstate = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentVote
|
model = AssignmentVote
|
||||||
fields = ("weight", "value")
|
fields = ("pollstate",) + BASE_VOTE_FIELDS
|
||||||
|
read_only_fields = BASE_VOTE_FIELDS
|
||||||
|
|
||||||
|
def get_pollstate(self, vote):
|
||||||
|
return vote.option.poll.state
|
||||||
|
|
||||||
|
|
||||||
class AssignmentOptionSerializer(ModelSerializer):
|
class AssignmentOptionSerializer(ModelSerializer):
|
||||||
@ -66,24 +68,21 @@ class AssignmentOptionSerializer(ModelSerializer):
|
|||||||
Serializer for assignment.models.AssignmentOption objects.
|
Serializer for assignment.models.AssignmentOption objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
votes = AssignmentVoteSerializer(many=True, read_only=True)
|
yes = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
|
||||||
is_elected = SerializerMethodField()
|
no = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
|
||||||
|
abstain = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
votes = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentOption
|
model = AssignmentOption
|
||||||
fields = ("id", "candidate", "is_elected", "votes", "poll", "weight")
|
fields = ("user",) + BASE_OPTION_FIELDS
|
||||||
|
read_only_fields = ("user",) + BASE_OPTION_FIELDS
|
||||||
def get_is_elected(self, obj):
|
|
||||||
"""
|
|
||||||
Returns the election status of the candidate of this option.
|
|
||||||
If the candidate is None (e.g. deleted) the result is False.
|
|
||||||
"""
|
|
||||||
if not obj.candidate:
|
|
||||||
return False
|
|
||||||
return obj.poll.assignment.is_elected(obj.candidate)
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentAllPollSerializer(ModelSerializer):
|
class AssignmentPollSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.AssignmentPoll objects.
|
Serializer for assignment.models.AssignmentPoll objects.
|
||||||
|
|
||||||
@ -91,103 +90,42 @@ class AssignmentAllPollSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
options = AssignmentOptionSerializer(many=True, read_only=True)
|
options = AssignmentOptionSerializer(many=True, read_only=True)
|
||||||
votes = ListField(
|
|
||||||
child=DictField(
|
title = CharField(allow_blank=False, required=True)
|
||||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)
|
groups = IdPrimaryKeyRelatedField(
|
||||||
),
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
write_only=True,
|
)
|
||||||
required=False,
|
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
|
votesvalid = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
votesinvalid = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
votescast = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
)
|
)
|
||||||
has_votes = SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentPoll
|
model = AssignmentPoll
|
||||||
fields = (
|
fields = (
|
||||||
"id",
|
|
||||||
"pollmethod",
|
|
||||||
"description",
|
|
||||||
"published",
|
|
||||||
"options",
|
|
||||||
"votesabstain",
|
|
||||||
"votesno",
|
|
||||||
"votesvalid",
|
|
||||||
"votesinvalid",
|
|
||||||
"votescast",
|
|
||||||
"votes",
|
|
||||||
"has_votes",
|
|
||||||
"assignment",
|
"assignment",
|
||||||
) # js-data needs the assignment-id in the nested object to define relations.
|
"pollmethod",
|
||||||
read_only_fields = ("pollmethod",)
|
"votes_amount",
|
||||||
validators = (default_votes_validator,)
|
"allow_multiple_votes_per_candidate",
|
||||||
|
"global_no",
|
||||||
|
"global_abstain",
|
||||||
|
) + BASE_POLL_FIELDS
|
||||||
|
read_only_fields = ("state",)
|
||||||
|
|
||||||
def get_has_votes(self, obj):
|
|
||||||
"""
|
|
||||||
Returns True if this poll has some votes.
|
|
||||||
"""
|
|
||||||
return obj.has_votes()
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""
|
""" Prevent from updating the assignment """
|
||||||
Customized update method for polls. To update votes use the write
|
validated_data.pop("assignment", None)
|
||||||
only field 'votes'.
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
Example data for a 'pollmethod'='yna' poll with two candidates:
|
|
||||||
|
|
||||||
"votes": [{"Yes": 10, "No": 4, "Abstain": -2},
|
|
||||||
{"Yes": -1, "No": 0, "Abstain": -2}]
|
|
||||||
|
|
||||||
Example data for a 'pollmethod' ='yn' poll with two candidates:
|
|
||||||
"votes": [{"Votes": 10}, {"Votes": 0}]
|
|
||||||
"""
|
|
||||||
# Update votes.
|
|
||||||
votes = validated_data.get("votes")
|
|
||||||
if votes:
|
|
||||||
options = list(instance.get_options())
|
|
||||||
if len(votes) != len(options):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "You have to submit data for {0} candidates.",
|
|
||||||
"args": [len(options)],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for index, option in enumerate(options):
|
|
||||||
if len(votes[index]) != len(instance.get_vote_values()):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "You have to submit data for {0} vote values",
|
|
||||||
"args": [len(instance.get_vote_values())],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for vote_value, __ in votes[index].items():
|
|
||||||
if vote_value not in instance.get_vote_values():
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "Vote value {0} is invalid.",
|
|
||||||
"args": [vote_value],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
instance.set_vote_objects_with_values(
|
|
||||||
option, votes[index], skip_autoupdate=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update remaining writeable fields.
|
|
||||||
instance.description = validated_data.get("description", instance.description)
|
|
||||||
instance.published = validated_data.get("published", instance.published)
|
|
||||||
instance.votesabstain = validated_data.get(
|
|
||||||
"votesabstain", instance.votesabstain
|
|
||||||
)
|
|
||||||
instance.votesno = validated_data.get("votesno", instance.votesno)
|
|
||||||
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
|
||||||
instance.votesinvalid = validated_data.get(
|
|
||||||
"votesinvalid", instance.votesinvalid
|
|
||||||
)
|
|
||||||
instance.votescast = validated_data.get("votescast", instance.votescast)
|
|
||||||
instance.save()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentFullSerializer(ModelSerializer):
|
class AssignmentSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.Assignment objects. With all polls.
|
Serializer for assignment.models.Assignment objects. With all polls.
|
||||||
"""
|
"""
|
||||||
@ -195,7 +133,7 @@ class AssignmentFullSerializer(ModelSerializer):
|
|||||||
assignment_related_users = AssignmentRelatedUserSerializer(
|
assignment_related_users = AssignmentRelatedUserSerializer(
|
||||||
many=True, read_only=True
|
many=True, read_only=True
|
||||||
)
|
)
|
||||||
polls = AssignmentAllPollSerializer(many=True, read_only=True)
|
polls = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
agenda_create = BooleanField(write_only=True, required=False, allow_null=True)
|
agenda_create = BooleanField(write_only=True, required=False, allow_null=True)
|
||||||
agenda_type = IntegerField(
|
agenda_type = IntegerField(
|
||||||
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
from openslides.poll.views import BasePollViewSet, BaseVoteViewSet
|
||||||
|
from openslides.utils.auth import has_perm
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
DestroyModelMixin,
|
|
||||||
GenericViewSet,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
UpdateModelMixin,
|
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
|
from openslides.utils.utils import is_int
|
||||||
|
|
||||||
from ..utils.auth import has_perm
|
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
from .models import Assignment, AssignmentPoll, AssignmentRelatedUser
|
from .models import Assignment, AssignmentPoll, AssignmentRelatedUser, AssignmentVote
|
||||||
from .serializers import AssignmentAllPollSerializer
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -48,7 +48,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"update",
|
"update",
|
||||||
"destroy",
|
"destroy",
|
||||||
"mark_elected",
|
"mark_elected",
|
||||||
"create_poll",
|
|
||||||
"sort_related_users",
|
"sort_related_users",
|
||||||
):
|
):
|
||||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
@ -98,7 +97,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
# To nominate self during voting you have to be a manager.
|
# To nominate self during voting you have to be a manager.
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
# If the request.user is already a candidate he can nominate himself nevertheless.
|
# If the request.user is already a candidate he can nominate himself nevertheless.
|
||||||
assignment.set_candidate(request.user)
|
assignment.add_candidate(request.user)
|
||||||
# Send new candidate via autoupdate because users without permission
|
# Send new candidate via autoupdate because users without permission
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data([request.user])
|
inform_changed_data([request.user])
|
||||||
@ -121,7 +120,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "You are not a candidate of this election."}
|
{"detail": "You are not a candidate of this election."}
|
||||||
)
|
)
|
||||||
assignment.delete_related_user(request.user)
|
assignment.remove_candidate(request.user)
|
||||||
return "You have withdrawn your candidature successfully."
|
return "You have withdrawn your candidature successfully."
|
||||||
|
|
||||||
def get_user_from_request_data(self, request):
|
def get_user_from_request_data(self, request):
|
||||||
@ -186,7 +185,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "User {0} is already nominated.", "args": [str(user)]}
|
{"detail": "User {0} is already nominated.", "args": [str(user)]}
|
||||||
)
|
)
|
||||||
assignment.set_candidate(user)
|
assignment.add_candidate(user)
|
||||||
# Send new candidate via autoupdate because users without permission
|
# Send new candidate via autoupdate because users without permission
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data(user)
|
inform_changed_data(user)
|
||||||
@ -211,7 +210,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"args": [str(user)],
|
"args": [str(user)],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assignment.delete_related_user(user)
|
assignment.remove_candidate(user)
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
|
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
|
||||||
)
|
)
|
||||||
@ -243,7 +242,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"args": [str(user)],
|
"args": [str(user)],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assignment.set_candidate(user)
|
assignment.add_candidate(user)
|
||||||
message = "User {0} was successfully unelected."
|
message = "User {0} was successfully unelected."
|
||||||
return Response({"detail": message, "args": [str(user)]})
|
return Response({"detail": message, "args": [str(user)]})
|
||||||
|
|
||||||
@ -309,7 +308,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
return Response({"detail": "Assignment related users successfully sorted."})
|
return Response({"detail": "Assignment related users successfully sorted."})
|
||||||
|
|
||||||
|
|
||||||
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
class AssignmentPollViewSet(BasePollViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for assignment polls.
|
API endpoint for assignment polls.
|
||||||
|
|
||||||
@ -317,12 +316,246 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = AssignmentPoll.objects.all()
|
queryset = AssignmentPoll.objects.all()
|
||||||
serializer_class = AssignmentAllPollSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def has_manage_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
return has_perm(self.request.user, "assignments.can_see") and has_perm(
|
return has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
self.request.user, "assignments.can_manage"
|
self.request.user, "assignments.can_manage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
assignment = serializer.validated_data["assignment"]
|
||||||
|
if not assignment.candidates.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Can not create poll because there are no candidates."}
|
||||||
|
)
|
||||||
|
|
||||||
|
super().perform_create(serializer)
|
||||||
|
|
||||||
|
def handle_analog_vote(self, data, poll, user):
|
||||||
|
"""
|
||||||
|
Request data:
|
||||||
|
{
|
||||||
|
"options": {<option_id>: {"Y": <amount>, ["N": <amount>], ["A": <amount>] }},
|
||||||
|
["votesvalid": <amount>], ["votesinvalid": <amount>], ["votescast": <amount>],
|
||||||
|
["global_no": <amount>], ["global_abstain": <amount>]
|
||||||
|
}
|
||||||
|
All amounts are decimals as strings
|
||||||
|
|
||||||
|
required fields per pollmethod:
|
||||||
|
- votes: Y
|
||||||
|
- YN: YN
|
||||||
|
- YNA: YNA
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValidationError({"detail": "Data must be a dict"})
|
||||||
|
|
||||||
|
options_data = data.get("options")
|
||||||
|
if not isinstance(options_data, dict):
|
||||||
|
raise ValidationError({"detail": "You must provide options"})
|
||||||
|
|
||||||
|
for key, value in options_data.items():
|
||||||
|
if not is_int(key):
|
||||||
|
raise ValidationError({"detail": "Keys must be int"})
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValidationError({"detail": "A dict per option is required"})
|
||||||
|
self.parse_decimal_value(value.get("Y"), min_value=-2)
|
||||||
|
if poll.pollmethod in (
|
||||||
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
|
):
|
||||||
|
self.parse_decimal_value(value.get("N"), min_value=-2)
|
||||||
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA:
|
||||||
|
self.parse_decimal_value(value.get("A"), min_value=-2)
|
||||||
|
|
||||||
|
# Check and set votes* values here, because this might raise errors.
|
||||||
|
if "votesvalid" in data:
|
||||||
|
poll.votesvalid = self.parse_decimal_value(data["votesvalid"], min_value=-2)
|
||||||
|
if "votesinvalid" in data:
|
||||||
|
poll.votesinvalid = self.parse_decimal_value(
|
||||||
|
data["votesinvalid"], min_value=-2
|
||||||
|
)
|
||||||
|
if "votescast" in data:
|
||||||
|
poll.votescast = self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
|
||||||
|
global_no_enabled = (
|
||||||
|
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
|
)
|
||||||
|
global_abstain_enabled = (
|
||||||
|
poll.global_abstain and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
|
)
|
||||||
|
if "global_no" in data and global_no_enabled:
|
||||||
|
self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
if "global_abstain" in data and global_abstain_enabled:
|
||||||
|
self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
|
||||||
|
options = poll.get_options()
|
||||||
|
|
||||||
|
# Check, if all options were given
|
||||||
|
db_option_ids = set(option.id for option in options)
|
||||||
|
data_option_ids = set(int(option_id) for option_id in options_data.keys())
|
||||||
|
if data_option_ids != db_option_ids:
|
||||||
|
raise ValidationError(
|
||||||
|
{"error": "You have to provide values for all options"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: make this atomic
|
||||||
|
for option_id, vote in options_data.items():
|
||||||
|
option = options.get(pk=int(option_id))
|
||||||
|
Y = self.parse_decimal_value(vote["Y"], min_value=-2)
|
||||||
|
AssignmentVote.objects.create(option=option, value="Y", weight=Y)
|
||||||
|
|
||||||
|
if poll.pollmethod in (
|
||||||
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
|
):
|
||||||
|
N = self.parse_decimal_value(vote["N"], min_value=-2)
|
||||||
|
AssignmentVote.objects.create(option=option, value="N", weight=N)
|
||||||
|
|
||||||
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA:
|
||||||
|
A = self.parse_decimal_value(vote["A"], min_value=-2)
|
||||||
|
AssignmentVote.objects.create(option=option, value="A", weight=A)
|
||||||
|
|
||||||
|
# Create votes for global no and global abstain
|
||||||
|
first_option = options.first()
|
||||||
|
if "global_no" in data and global_no_enabled:
|
||||||
|
global_no = self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
AssignmentVote.objects.create(
|
||||||
|
option=first_option, value="N", weight=global_no
|
||||||
|
)
|
||||||
|
if "global_abstain" in data and global_abstain_enabled:
|
||||||
|
global_abstain = self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
AssignmentVote.objects.create(
|
||||||
|
option=first_option, value="A", weight=global_abstain
|
||||||
|
)
|
||||||
|
|
||||||
|
poll.state = AssignmentPoll.STATE_FINISHED # directly stop the poll
|
||||||
|
poll.save()
|
||||||
|
|
||||||
|
def validate_vote_data(self, data, poll):
|
||||||
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
amount_sum = 0
|
||||||
|
for option_id, amount in data.items():
|
||||||
|
if not is_int(option_id):
|
||||||
|
raise ValidationError({"detail": "Each id must be an int."})
|
||||||
|
if not is_int(amount):
|
||||||
|
raise ValidationError({"detail": "Each amounts must be int"})
|
||||||
|
amount = int(amount)
|
||||||
|
if amount < 1:
|
||||||
|
raise ValidationError({"detail": "At least 1 vote per option"})
|
||||||
|
if not poll.allow_multiple_votes_per_candidate and amount != 1:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Multiple votes are not allowed"}
|
||||||
|
)
|
||||||
|
amount_sum += amount
|
||||||
|
|
||||||
|
if amount_sum != poll.votes_amount:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": "You have to give exactly {0} votes",
|
||||||
|
"args": [poll.votes_amount],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Check, if all options are valid
|
||||||
|
db_option_ids = set(option.id for option in poll.get_options())
|
||||||
|
data_option_ids = set(int(option_id) for option_id in data.keys())
|
||||||
|
if len(data_option_ids - db_option_ids):
|
||||||
|
raise ValidationError({"error": "There are invalid option ids."})
|
||||||
|
elif data == "N" and poll.global_no:
|
||||||
|
pass
|
||||||
|
elif data == "A" and poll.global_abstain:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValidationError({"detail": "invalid data."})
|
||||||
|
|
||||||
|
elif poll.pollmethod in (
|
||||||
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
|
):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValidationError({"detail": "Data must be a dict."})
|
||||||
|
for option_id, value in data.items():
|
||||||
|
if not is_int(option_id):
|
||||||
|
raise ValidationError({"detail": "Keys must be int"})
|
||||||
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA and value not in (
|
||||||
|
"Y",
|
||||||
|
"N",
|
||||||
|
"A",
|
||||||
|
):
|
||||||
|
raise ValidationError("Every value must be Y, N or A")
|
||||||
|
elif poll.pollmethod == AssignmentPoll.POLLMETHOD_YN and value not in (
|
||||||
|
"Y",
|
||||||
|
"N",
|
||||||
|
):
|
||||||
|
raise ValidationError("Every value must be Y or N")
|
||||||
|
|
||||||
|
# Check, if all options were given
|
||||||
|
db_option_ids = set(option.id for option in poll.get_options())
|
||||||
|
data_option_ids = set(int(option_id) for option_id in data.keys())
|
||||||
|
if data_option_ids != db_option_ids:
|
||||||
|
raise ValidationError(
|
||||||
|
{"error": "You have to provide values for all options"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_votes(self, data, poll, user=None):
|
||||||
|
options = poll.get_options()
|
||||||
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for option_id, amount in data.items():
|
||||||
|
option = options.get(pk=option_id)
|
||||||
|
vote = AssignmentVote.objects.create(
|
||||||
|
option=option, user=user, weight=Decimal(amount), value="Y"
|
||||||
|
)
|
||||||
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||||
|
else:
|
||||||
|
option = options.first()
|
||||||
|
vote = AssignmentVote.objects.create(
|
||||||
|
option=option, user=user, weight=Decimal(1), value=data
|
||||||
|
)
|
||||||
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||||
|
elif poll.pollmethod in (
|
||||||
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
def handle_named_vote(self, data, poll, user):
|
||||||
|
"""
|
||||||
|
Request data for votes pollmethod:
|
||||||
|
{<option_id>: <amount>} | 'N' | 'A'
|
||||||
|
- Exactly one of the three options must be given
|
||||||
|
- 'N' is only valid if poll.global_no==True
|
||||||
|
- 'A' is only valid if poll.global_abstain==True
|
||||||
|
- amonts must be integer numbers >= 1.
|
||||||
|
- ids should be integers of valid option ids for this poll
|
||||||
|
- amounts must be one ("1"), if poll.allow_multiple_votes_per_candidate if False
|
||||||
|
- The sum of all amounts must be poll.votes_amount votes
|
||||||
|
|
||||||
|
Request data for YN/YNA pollmethod:
|
||||||
|
{<option_id>: 'Y' | 'N' [|'A']}
|
||||||
|
- all option_ids must be given
|
||||||
|
- 'A' is only allowed in YNA pollmethod
|
||||||
|
"""
|
||||||
|
self.validate_vote_data(data, poll)
|
||||||
|
# Instead of reusing all existing votes for the user, delete all previous votes
|
||||||
|
for vote in poll.get_votes().filter(user=user):
|
||||||
|
vote.delete()
|
||||||
|
self.create_votes(data, poll, user)
|
||||||
|
|
||||||
|
def handle_pseudoanonymous_vote(self, data, poll):
|
||||||
|
"""
|
||||||
|
For request data see handle_named_vote
|
||||||
|
"""
|
||||||
|
self.validate_vote_data(data, poll)
|
||||||
|
self.create_votes(data, poll)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentVoteViewSet(BaseVoteViewSet):
|
||||||
|
queryset = AssignmentVote.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
return has_perm(self.request.user, "assignments.can_see")
|
||||||
|
10
openslides/core/migrations/0026_remove_history_restricted.py
Normal file
10
openslides/core/migrations/0026_remove_history_restricted.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-28 11:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0025_projector_color")]
|
||||||
|
|
||||||
|
operations = [migrations.RemoveField(model_name="history", name="restricted")]
|
@ -273,7 +273,6 @@ class HistoryManager(models.Manager):
|
|||||||
),
|
),
|
||||||
now=history_time,
|
now=history_time,
|
||||||
information=element.get("information", []),
|
information=element.get("information", []),
|
||||||
restricted=element.get("restricted", False),
|
|
||||||
user_id=element.get("user_id"),
|
user_id=element.get("user_id"),
|
||||||
full_data=data,
|
full_data=data,
|
||||||
)
|
)
|
||||||
@ -324,8 +323,6 @@ class History(models.Model):
|
|||||||
|
|
||||||
information = JSONField()
|
information = JSONField()
|
||||||
|
|
||||||
restricted = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
|
@ -179,3 +179,64 @@ class StateAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
|
|
||||||
|
class MotionPollAccessPermissions(BaseAccessPermissions):
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
STATE_PUBLISHED = 4
|
||||||
|
|
||||||
|
async def get_restricted_data(
|
||||||
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Poll-managers have full access, even during an active poll.
|
||||||
|
Non-published polls will be restricted:
|
||||||
|
- Remove votes* values from the poll
|
||||||
|
- Remove yes/no/abstain fields from options
|
||||||
|
- Remove voted_id field from the poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
if await async_has_perm(user_id, "motions.can_manage_polls"):
|
||||||
|
data = full_data
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
for poll in full_data:
|
||||||
|
if poll["state"] != self.STATE_PUBLISHED:
|
||||||
|
poll = json.loads(
|
||||||
|
json.dumps(poll)
|
||||||
|
) # copy, so we can remove some fields.
|
||||||
|
del poll["votesvalid"]
|
||||||
|
del poll["votesinvalid"]
|
||||||
|
del poll["votescast"]
|
||||||
|
del poll["voted_id"]
|
||||||
|
for option in poll["options"]:
|
||||||
|
del option["yes"]
|
||||||
|
del option["no"]
|
||||||
|
del option["abstain"]
|
||||||
|
data.append(poll)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class MotionVoteAccessPermissions(BaseAccessPermissions):
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
STATE_PUBLISHED = 4
|
||||||
|
|
||||||
|
async def get_restricted_data(
|
||||||
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Poll-managers have full access, even during an active poll.
|
||||||
|
Every user can see it's own votes.
|
||||||
|
If the pollstate is published, everyone can see the votes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if await async_has_perm(user_id, "motions.can_manage_polls"):
|
||||||
|
data = full_data
|
||||||
|
else:
|
||||||
|
data = [
|
||||||
|
vote
|
||||||
|
for vote in full_data
|
||||||
|
if vote["pollstate"] == self.STATE_PUBLISHED
|
||||||
|
or vote["user_id"] == user_id
|
||||||
|
]
|
||||||
|
return data
|
||||||
|
@ -20,6 +20,7 @@ class MotionsAppConfig(AppConfig):
|
|||||||
StatuteParagraphViewSet,
|
StatuteParagraphViewSet,
|
||||||
MotionViewSet,
|
MotionViewSet,
|
||||||
MotionCommentSectionViewSet,
|
MotionCommentSectionViewSet,
|
||||||
|
MotionVoteViewSet,
|
||||||
MotionBlockViewSet,
|
MotionBlockViewSet,
|
||||||
MotionPollViewSet,
|
MotionPollViewSet,
|
||||||
MotionChangeRecommendationViewSet,
|
MotionChangeRecommendationViewSet,
|
||||||
@ -66,6 +67,9 @@ class MotionsAppConfig(AppConfig):
|
|||||||
router.register(
|
router.register(
|
||||||
self.get_model("MotionPoll").get_collection_string(), MotionPollViewSet
|
self.get_model("MotionPoll").get_collection_string(), MotionPollViewSet
|
||||||
)
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("MotionVote").get_collection_string(), MotionVoteViewSet
|
||||||
|
)
|
||||||
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
||||||
|
|
||||||
# Register required_users
|
# Register required_users
|
||||||
@ -92,6 +96,8 @@ class MotionsAppConfig(AppConfig):
|
|||||||
"State",
|
"State",
|
||||||
"MotionChangeRecommendation",
|
"MotionChangeRecommendation",
|
||||||
"MotionCommentSection",
|
"MotionCommentSection",
|
||||||
|
"MotionPoll",
|
||||||
|
"MotionVote",
|
||||||
):
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
146
openslides/motions/migrations/0033_auto_20191017_1100.py
Normal file
146
openslides/motions/migrations/0033_auto_20191017_1100.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-17 09:00
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0011_postgresql_auth_group_id_sequence"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("motions", "0032_category_cascade_delete"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(blank=True, to="users.Group"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="state",
|
||||||
|
field=models.IntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Created"),
|
||||||
|
(2, "Started"),
|
||||||
|
(3, "Finished"),
|
||||||
|
(4, "Published"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(default="Poll", blank=True, max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("analog", "Analog"),
|
||||||
|
("named", "Named"),
|
||||||
|
("pseudoanonymous", "Pseudoanonymous"),
|
||||||
|
],
|
||||||
|
default="analog",
|
||||||
|
max_length=64,
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="pollmethod",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("YN", "YN"), ("YNA", "YNA")], default="YNA", max_length=3
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="voted",
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="option",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="votes",
|
||||||
|
to="motions.MotionOption",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="value",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("Y", "Y"), ("N", "N"), ("A", "A")], max_length=1
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="weight",
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=6,
|
||||||
|
default=Decimal("1"),
|
||||||
|
max_digits=15,
|
||||||
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionoption",
|
||||||
|
name="poll",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="options",
|
||||||
|
to="motions.MotionPoll",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll", old_name="votescast", new_name="db_votescast"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll", old_name="votesinvalid", new_name="db_votesinvalid"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll", old_name="votesvalid", new_name="db_votesvalid"
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="motion",
|
||||||
|
options={
|
||||||
|
"default_permissions": (),
|
||||||
|
"ordering": ("identifier",),
|
||||||
|
"permissions": (
|
||||||
|
("can_see", "Can see motions"),
|
||||||
|
("can_see_internal", "Can see motions in internal state"),
|
||||||
|
("can_create", "Can create motions"),
|
||||||
|
("can_create_amendments", "Can create amendments"),
|
||||||
|
("can_support", "Can support motions"),
|
||||||
|
("can_manage_metadata", "Can manage motion metadata"),
|
||||||
|
("can_manage", "Can manage motions"),
|
||||||
|
("can_manage_polls", "Can manage motion polls"),
|
||||||
|
),
|
||||||
|
"verbose_name": "Motion",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -8,12 +8,7 @@ from openslides.agenda.mixins import AgendaItemWithListOfSpeakersMixin
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.poll.models import (
|
from openslides.poll.models import BaseOption, BasePoll, BaseVote
|
||||||
BaseOption,
|
|
||||||
BasePoll,
|
|
||||||
BaseVote,
|
|
||||||
CollectDefaultVotesMixin,
|
|
||||||
)
|
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
@ -26,6 +21,8 @@ from .access_permissions import (
|
|||||||
MotionBlockAccessPermissions,
|
MotionBlockAccessPermissions,
|
||||||
MotionChangeRecommendationAccessPermissions,
|
MotionChangeRecommendationAccessPermissions,
|
||||||
MotionCommentSectionAccessPermissions,
|
MotionCommentSectionAccessPermissions,
|
||||||
|
MotionPollAccessPermissions,
|
||||||
|
MotionVoteAccessPermissions,
|
||||||
StateAccessPermissions,
|
StateAccessPermissions,
|
||||||
StatuteParagraphAccessPermissions,
|
StatuteParagraphAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
@ -80,6 +77,11 @@ class MotionManager(models.Manager):
|
|||||||
"agenda_items",
|
"agenda_items",
|
||||||
"lists_of_speakers",
|
"lists_of_speakers",
|
||||||
"polls",
|
"polls",
|
||||||
|
"polls__groups",
|
||||||
|
"polls__voted",
|
||||||
|
"polls__options",
|
||||||
|
"polls__options__votes",
|
||||||
|
"polls__options__votes__user",
|
||||||
"attachments",
|
"attachments",
|
||||||
"tags",
|
"tags",
|
||||||
"submitters",
|
"submitters",
|
||||||
@ -269,6 +271,7 @@ class Motion(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
|
|||||||
("can_support", "Can support motions"),
|
("can_support", "Can support motions"),
|
||||||
("can_manage_metadata", "Can manage motion metadata"),
|
("can_manage_metadata", "Can manage motion metadata"),
|
||||||
("can_manage", "Can manage motions"),
|
("can_manage", "Can manage motions"),
|
||||||
|
("can_manage_polls", "Can manage motion polls"),
|
||||||
)
|
)
|
||||||
ordering = ("identifier",)
|
ordering = ("identifier",)
|
||||||
verbose_name = "Motion"
|
verbose_name = "Motion"
|
||||||
@ -424,22 +427,6 @@ class Motion(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
return user in self.supporters.all()
|
return user in self.supporters.all()
|
||||||
|
|
||||||
def create_poll(self, skip_autoupdate=False):
|
|
||||||
"""
|
|
||||||
Create a new poll for this motion.
|
|
||||||
|
|
||||||
Return the new poll object.
|
|
||||||
"""
|
|
||||||
if self.state.allow_create_poll:
|
|
||||||
poll = MotionPoll(motion=self)
|
|
||||||
poll.save(skip_autoupdate=skip_autoupdate)
|
|
||||||
poll.set_options(skip_autoupdate=skip_autoupdate)
|
|
||||||
return poll
|
|
||||||
else:
|
|
||||||
raise WorkflowError(
|
|
||||||
f"You can not create a poll in state {self.state.name}."
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def workflow_id(self):
|
def workflow_id(self):
|
||||||
"""
|
"""
|
||||||
@ -881,82 +868,48 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode
|
|||||||
|
|
||||||
|
|
||||||
class MotionVote(RESTModelMixin, BaseVote):
|
class MotionVote(RESTModelMixin, BaseVote):
|
||||||
"""Saves the votes for a MotionPoll.
|
access_permissions = MotionVoteAccessPermissions()
|
||||||
|
option = models.ForeignKey(
|
||||||
There should allways be three MotionVote objects for each poll,
|
"MotionOption", on_delete=models.CASCADE, related_name="votes"
|
||||||
one for 'yes', 'no', and 'abstain'."""
|
)
|
||||||
|
|
||||||
option = models.ForeignKey("MotionOption", on_delete=models.CASCADE)
|
|
||||||
"""The option object, to witch the vote belongs."""
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
|
||||||
"""
|
|
||||||
Returns the motion to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.option.poll.motion
|
|
||||||
|
|
||||||
|
|
||||||
class MotionOption(RESTModelMixin, BaseOption):
|
class MotionOption(RESTModelMixin, BaseOption):
|
||||||
"""Links between the MotionPollClass and the MotionVoteClass.
|
|
||||||
|
|
||||||
There should be one MotionOption object for each poll."""
|
|
||||||
|
|
||||||
poll = models.ForeignKey("MotionPoll", on_delete=models.CASCADE)
|
|
||||||
"""The poll object, to witch the object belongs."""
|
|
||||||
|
|
||||||
vote_class = MotionVote
|
vote_class = MotionVote
|
||||||
"""The VoteClass, to witch this Class links."""
|
|
||||||
|
poll = models.ForeignKey(
|
||||||
|
"MotionPoll", related_name="options", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
return self.poll
|
||||||
Returns the motion to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.poll.motion
|
|
||||||
|
|
||||||
|
|
||||||
|
# Meta-TODO: Is this todo resolved?
|
||||||
# TODO: remove the type-ignoring in the next line, after this is solved:
|
# TODO: remove the type-ignoring in the next line, after this is solved:
|
||||||
# https://github.com/python/mypy/issues/3855
|
# https://github.com/python/mypy/issues/3855
|
||||||
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore
|
class MotionPoll(RESTModelMixin, BasePoll):
|
||||||
"""The Class to saves the vote result for a motion poll."""
|
access_permissions = MotionPollAccessPermissions()
|
||||||
|
option_class = MotionOption
|
||||||
|
|
||||||
motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
|
motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
|
||||||
"""The motion to witch the object belongs."""
|
|
||||||
|
|
||||||
option_class = MotionOption
|
POLLMETHOD_YN = "YN"
|
||||||
"""The option class, witch links between this object the the votes."""
|
POLLMETHOD_YNA = "YNA"
|
||||||
|
POLLMETHODS = (("YN", "YN"), ("YNA", "YNA"))
|
||||||
vote_values = ["Yes", "No", "Abstain"]
|
pollmethod = models.CharField(max_length=3, choices=POLLMETHODS)
|
||||||
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def __str__(self):
|
def create_options(self):
|
||||||
"""
|
MotionOption.objects.create(poll=self)
|
||||||
Representation method only for debugging purposes.
|
|
||||||
"""
|
|
||||||
return f"MotionPoll for motion {self.motion}"
|
|
||||||
|
|
||||||
def set_options(self, skip_autoupdate=False):
|
|
||||||
"""Create the option class for this poll."""
|
|
||||||
# TODO: maybe it is possible with .create() to call this without poll=self
|
|
||||||
# or call this in save()
|
|
||||||
self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate)
|
|
||||||
|
|
||||||
def get_percent_base_choice(self):
|
|
||||||
return config["motions_poll_100_percent_base"]
|
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
|
||||||
"""
|
|
||||||
Returns the motion to this instance which is the root REST element.
|
|
||||||
"""
|
|
||||||
return self.motion
|
|
||||||
|
|
||||||
|
|
||||||
class State(RESTModelMixin, models.Model):
|
class State(RESTModelMixin, models.Model):
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
from openslides.poll.serializers import (
|
||||||
|
BASE_OPTION_FIELDS,
|
||||||
|
BASE_POLL_FIELDS,
|
||||||
|
BASE_VOTE_FIELDS,
|
||||||
|
)
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..poll.serializers import default_votes_validator
|
|
||||||
from ..utils.auth import get_group_model, has_perm
|
from ..utils.auth import get_group_model, has_perm
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
CharField,
|
CharField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
DictField,
|
|
||||||
Field,
|
Field,
|
||||||
IdPrimaryKeyRelatedField,
|
IdPrimaryKeyRelatedField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
@ -28,7 +30,9 @@ from .models import (
|
|||||||
MotionChangeRecommendation,
|
MotionChangeRecommendation,
|
||||||
MotionComment,
|
MotionComment,
|
||||||
MotionCommentSection,
|
MotionCommentSection,
|
||||||
|
MotionOption,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
|
MotionVote,
|
||||||
State,
|
State,
|
||||||
StatuteParagraph,
|
StatuteParagraph,
|
||||||
Submitter,
|
Submitter,
|
||||||
@ -220,116 +224,65 @@ class AmendmentParagraphsJSONSerializerField(Field):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class MotionVoteSerializer(ModelSerializer):
|
||||||
|
pollstate = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MotionVote
|
||||||
|
fields = ("pollstate",) + BASE_VOTE_FIELDS
|
||||||
|
read_only_fields = BASE_VOTE_FIELDS
|
||||||
|
|
||||||
|
def get_pollstate(self, vote):
|
||||||
|
return vote.option.poll.state
|
||||||
|
|
||||||
|
|
||||||
|
class MotionOptionSerializer(ModelSerializer):
|
||||||
|
yes = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
|
||||||
|
no = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
|
||||||
|
abstain = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
votes = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MotionOption
|
||||||
|
fields = BASE_OPTION_FIELDS
|
||||||
|
read_only_fields = BASE_OPTION_FIELDS
|
||||||
|
|
||||||
|
|
||||||
class MotionPollSerializer(ModelSerializer):
|
class MotionPollSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionPoll objects.
|
Serializer for motion.models.MotionPoll objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yes = SerializerMethodField()
|
options = MotionOptionSerializer(many=True, read_only=True)
|
||||||
no = SerializerMethodField()
|
|
||||||
abstain = SerializerMethodField()
|
title = CharField(allow_blank=False, required=True)
|
||||||
votes = DictField(
|
groups = IdPrimaryKeyRelatedField(
|
||||||
child=DecimalField(
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
max_digits=15, decimal_places=6, min_value=-2, allow_null=True
|
)
|
||||||
),
|
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
write_only=True,
|
|
||||||
|
votesvalid = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
votesinvalid = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
|
)
|
||||||
|
votescast = DecimalField(
|
||||||
|
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||||
)
|
)
|
||||||
has_votes = SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionPoll
|
model = MotionPoll
|
||||||
fields = (
|
fields = ("motion", "pollmethod") + BASE_POLL_FIELDS
|
||||||
"id",
|
read_only_fields = ("state",)
|
||||||
"motion",
|
|
||||||
"yes",
|
|
||||||
"no",
|
|
||||||
"abstain",
|
|
||||||
"votesvalid",
|
|
||||||
"votesinvalid",
|
|
||||||
"votescast",
|
|
||||||
"votes",
|
|
||||||
"has_votes",
|
|
||||||
)
|
|
||||||
validators = (default_votes_validator,)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# The following dictionary is just a cache for several votes.
|
|
||||||
self._votes_dicts: Dict[int, Dict[int, int]] = {}
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_yes(self, obj):
|
|
||||||
try:
|
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)["Yes"])
|
|
||||||
except KeyError:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_no(self, obj):
|
|
||||||
try:
|
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)["No"])
|
|
||||||
except KeyError:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_abstain(self, obj):
|
|
||||||
try:
|
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)["Abstain"])
|
|
||||||
except KeyError:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_votes_dict(self, obj):
|
|
||||||
try:
|
|
||||||
votes_dict = self._votes_dicts[obj.pk]
|
|
||||||
except KeyError:
|
|
||||||
votes_dict = self._votes_dicts[obj.pk] = {}
|
|
||||||
for vote in obj.get_votes():
|
|
||||||
votes_dict[vote.value] = vote.weight
|
|
||||||
return votes_dict
|
|
||||||
|
|
||||||
def get_has_votes(self, obj):
|
|
||||||
"""
|
|
||||||
Returns True if this poll has some votes.
|
|
||||||
"""
|
|
||||||
return obj.has_votes()
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""
|
""" Prevent from updating the motion """
|
||||||
Customized update method for polls. To update votes use the write
|
validated_data.pop("motion", None)
|
||||||
only field 'votes'.
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
Example data:
|
|
||||||
|
|
||||||
"votes": {"Yes": 10, "No": 4, "Abstain": -2}
|
|
||||||
"""
|
|
||||||
# Update votes.
|
|
||||||
votes = validated_data.get("votes")
|
|
||||||
if votes:
|
|
||||||
if len(votes) != len(instance.get_vote_values()):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "You have to submit data for {0} vote values.",
|
|
||||||
"args": [len(instance.get_vote_values())],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for vote_value in votes.keys():
|
|
||||||
if vote_value not in instance.get_vote_values():
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "Vote value {0} is invalid.", "args": [vote_value]}
|
|
||||||
)
|
|
||||||
instance.set_vote_objects_with_values(
|
|
||||||
instance.get_options().get(), votes, skip_autoupdate=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update remaining writeable fields.
|
|
||||||
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
|
||||||
instance.votesinvalid = validated_data.get(
|
|
||||||
"votesinvalid", instance.votesinvalid
|
|
||||||
)
|
|
||||||
instance.votescast = validated_data.get("votescast", instance.votescast)
|
|
||||||
instance.save()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class MotionChangeRecommendationSerializer(ModelSerializer):
|
class MotionChangeRecommendationSerializer(ModelSerializer):
|
||||||
@ -418,7 +371,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
comments = MotionCommentSerializer(many=True, read_only=True)
|
comments = MotionCommentSerializer(many=True, read_only=True)
|
||||||
polls = MotionPollSerializer(many=True, read_only=True)
|
polls = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
modified_final_version = CharField(allow_blank=True, required=False)
|
modified_final_version = CharField(allow_blank=True, required=False)
|
||||||
reason = CharField(allow_blank=True, required=False)
|
reason = CharField(allow_blank=True, required=False)
|
||||||
state_restriction = SerializerMethodField()
|
state_restriction = SerializerMethodField()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from typing import List, Set
|
from typing import List, Set
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
@ -8,17 +9,16 @@ from django.db.models.deletion import ProtectedError
|
|||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from openslides.poll.views import BasePollViewSet, BaseVoteViewSet
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.models import Tag
|
from ..core.models import Tag
|
||||||
from ..utils.auth import has_perm, in_some_groups
|
from ..utils.auth import has_perm, in_some_groups
|
||||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
DestroyModelMixin,
|
|
||||||
GenericViewSet,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
ReturnDict,
|
ReturnDict,
|
||||||
UpdateModelMixin,
|
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
list_route,
|
list_route,
|
||||||
@ -34,7 +34,6 @@ from .access_permissions import (
|
|||||||
StatuteParagraphAccessPermissions,
|
StatuteParagraphAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
)
|
)
|
||||||
from .exceptions import WorkflowError
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
@ -43,13 +42,13 @@ from .models import (
|
|||||||
MotionComment,
|
MotionComment,
|
||||||
MotionCommentSection,
|
MotionCommentSection,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
|
MotionVote,
|
||||||
State,
|
State,
|
||||||
StatuteParagraph,
|
StatuteParagraph,
|
||||||
Submitter,
|
Submitter,
|
||||||
Workflow,
|
Workflow,
|
||||||
)
|
)
|
||||||
from .numbering import numbering
|
from .numbering import numbering
|
||||||
from .serializers import MotionPollSerializer
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -87,7 +86,6 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
"follow_recommendation",
|
"follow_recommendation",
|
||||||
"manage_multiple_submitters",
|
"manage_multiple_submitters",
|
||||||
"manage_multiple_tags",
|
"manage_multiple_tags",
|
||||||
"create_poll",
|
|
||||||
):
|
):
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage_metadata"
|
self.request.user, "motions.can_manage_metadata"
|
||||||
@ -400,9 +398,7 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
message = ["Comment {arg1} deleted", section.name]
|
message = ["Comment {arg1} deleted", section.name]
|
||||||
|
|
||||||
# Fire autoupdate again to save information to OpenSlides history.
|
# Fire autoupdate again to save information to OpenSlides history.
|
||||||
inform_changed_data(
|
inform_changed_data(motion, information=message, user_id=request.user.pk)
|
||||||
motion, information=message, user_id=request.user.pk, restricted=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response({"detail": message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@ -1042,31 +1038,6 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
|
|
||||||
return Response({"detail": "Recommendation followed successfully."})
|
return Response({"detail": "Recommendation followed successfully."})
|
||||||
|
|
||||||
@detail_route(methods=["post"])
|
|
||||||
def create_poll(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
View to create a poll. It is a POST request without any data.
|
|
||||||
"""
|
|
||||||
motion = self.get_object()
|
|
||||||
if not motion.state.allow_create_poll:
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "You can not create a poll in this motion state."}
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
poll = motion.create_poll(skip_autoupdate=True)
|
|
||||||
except WorkflowError as err:
|
|
||||||
raise ValidationError({"detail": err})
|
|
||||||
|
|
||||||
# Fire autoupdate again to save information to OpenSlides history.
|
|
||||||
inform_changed_data(
|
|
||||||
motion, information=["Vote created"], user_id=request.user.pk
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{"detail": "Vote created successfully.", "createdPollId": poll.pk}
|
|
||||||
)
|
|
||||||
|
|
||||||
@list_route(methods=["post"])
|
@list_route(methods=["post"])
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def manage_multiple_tags(self, request):
|
def manage_multiple_tags(self, request):
|
||||||
@ -1137,7 +1108,7 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
class MotionPollViewSet(BasePollViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for motion polls.
|
API endpoint for motion polls.
|
||||||
|
|
||||||
@ -1145,9 +1116,8 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = MotionPoll.objects.all()
|
queryset = MotionPoll.objects.all()
|
||||||
serializer_class = MotionPollSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def has_manage_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
@ -1155,16 +1125,30 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
|||||||
self.request.user, "motions.can_manage_metadata"
|
self.request.user, "motions.can_manage_metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
motion = serializer.validated_data["motion"]
|
||||||
|
if not motion.state.allow_create_poll:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You can not create a poll in this motion state."}
|
||||||
|
)
|
||||||
|
|
||||||
|
super().perform_create(serializer)
|
||||||
|
|
||||||
|
# Fire autoupdate again to save information to OpenSlides history.
|
||||||
|
inform_changed_data(
|
||||||
|
motion, information=["Poll created"], user_id=self.request.user.pk
|
||||||
|
)
|
||||||
|
|
||||||
def update(self, *args, **kwargs):
|
def update(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Customized view endpoint to update a motion poll.
|
Customized view endpoint to update a motion poll.
|
||||||
"""
|
"""
|
||||||
response = super().update(*args, **kwargs)
|
response = super().update(*args, **kwargs)
|
||||||
poll = self.get_object()
|
|
||||||
|
|
||||||
# Fire autoupdate again to save information to OpenSlides history.
|
# Fire autoupdate again to save information to OpenSlides history.
|
||||||
|
poll = self.get_object()
|
||||||
inform_changed_data(
|
inform_changed_data(
|
||||||
poll.motion, information=["Vote updated"], user_id=self.request.user.pk
|
poll.motion, information=["Poll updated"], user_id=self.request.user.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@ -1178,11 +1162,75 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
|||||||
|
|
||||||
# Fire autoupdate again to save information to OpenSlides history.
|
# Fire autoupdate again to save information to OpenSlides history.
|
||||||
inform_changed_data(
|
inform_changed_data(
|
||||||
poll.motion, information=["Vote deleted"], user_id=self.request.user.pk
|
poll.motion, information=["Poll deleted"], user_id=self.request.user.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def handle_analog_vote(self, data, poll, user):
|
||||||
|
"""
|
||||||
|
Request data:
|
||||||
|
{ "Y": <amount>, "N": <amount>, ["A": <amount>],
|
||||||
|
["votesvalid": <amount>], ["votesinvalid": <amount>], ["votescast": <amount>]}
|
||||||
|
All amounts are decimals as strings
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValidationError({"detail": "Data must be a dict"})
|
||||||
|
Y = self.parse_decimal_value(data.get("Y"), min_value=-2)
|
||||||
|
N = self.parse_decimal_value(data.get("N"), min_value=-2)
|
||||||
|
if poll.pollmethod == MotionPoll.POLLMETHOD_YNA:
|
||||||
|
A = self.parse_decimal_value(data.get("A"), min_value=-2)
|
||||||
|
|
||||||
|
option = poll.options.get()
|
||||||
|
MotionVote.objects.create(option=option, value="Y", weight=Y)
|
||||||
|
MotionVote.objects.create(option=option, value="N", weight=N)
|
||||||
|
if poll.pollmethod == MotionPoll.POLLMETHOD_YNA:
|
||||||
|
MotionVote.objects.create(option=option, value="A", weight=A)
|
||||||
|
|
||||||
|
if "votesvalid" in data:
|
||||||
|
poll.votesvalid = self.parse_decimal_value(data["votesvalid"], min_value=-2)
|
||||||
|
if "votesinvalid" in data:
|
||||||
|
poll.votesinvalid = self.parse_decimal_value(
|
||||||
|
data["votesinvalid"], min_value=-2
|
||||||
|
)
|
||||||
|
if "votescast" in data:
|
||||||
|
poll.votescast = self.parse_decimal_value(data["votescast"], min_value=-2)
|
||||||
|
|
||||||
|
poll.state = MotionPoll.STATE_FINISHED # directly stop the poll
|
||||||
|
poll.save()
|
||||||
|
|
||||||
|
def validate_vote_data(self, data, poll):
|
||||||
|
if poll.pollmethod == MotionPoll.POLLMETHOD_YNA and data not in ("Y", "N", "A"):
|
||||||
|
raise ValidationError("Data must be Y, N or A")
|
||||||
|
elif poll.pollmethod == MotionPoll.POLLMETHOD_YN and data not in ("Y", "N"):
|
||||||
|
raise ValidationError("Data must be Y or N")
|
||||||
|
|
||||||
|
def handle_named_vote(self, data, poll, user):
|
||||||
|
self.validate_vote_data(data, poll)
|
||||||
|
|
||||||
|
option = poll.options.get()
|
||||||
|
vote, _ = MotionVote.objects.get_or_create(user=user, option=option)
|
||||||
|
self.set_vote_data(data, vote, poll)
|
||||||
|
|
||||||
|
def handle_pseudoanonymous_vote(self, data, poll):
|
||||||
|
self.validate_vote_data(data, poll)
|
||||||
|
|
||||||
|
option = poll.options.get()
|
||||||
|
vote = MotionVote.objects.create(option=option)
|
||||||
|
self.set_vote_data(data, vote, poll)
|
||||||
|
|
||||||
|
def set_vote_data(self, data, vote, poll):
|
||||||
|
vote.value = data
|
||||||
|
vote.weight = Decimal("1")
|
||||||
|
vote.save(no_delete_on_restriction=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MotionVoteViewSet(BaseVoteViewSet):
|
||||||
|
queryset = MotionVote.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
return has_perm(self.request.user, "motions.can_see")
|
||||||
|
|
||||||
|
|
||||||
class MotionChangeRecommendationViewSet(ModelViewSet):
|
class MotionChangeRecommendationViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
@ -1620,7 +1668,6 @@ class StateViewSet(ModelViewSet, ProtectedErrorMessageMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = State.objects.all()
|
queryset = State.objects.all()
|
||||||
# serializer_class = StateSerializer
|
|
||||||
access_permissions = StateAccessPermissions()
|
access_permissions = StateAccessPermissions()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
|
@ -1,20 +1,44 @@
|
|||||||
import locale
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.conf import settings
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from ..utils.autoupdate import inform_deleted_data
|
||||||
|
from ..utils.models import SET_NULL_AND_AUTOUPDATE
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVote(models.Model):
|
||||||
|
"""
|
||||||
|
All subclasses must have option attribute with the related name "votes"
|
||||||
|
"""
|
||||||
|
|
||||||
|
weight = models.DecimalField(
|
||||||
|
default=Decimal("1"),
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
value = models.CharField(max_length=1, choices=(("Y", "Y"), ("N", "N"), ("A", "A")))
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def get_root_rest_element(self):
|
||||||
|
return self.option.get_root_rest_element()
|
||||||
|
|
||||||
|
|
||||||
class BaseOption(models.Model):
|
class BaseOption(models.Model):
|
||||||
"""
|
"""
|
||||||
Base option class for a poll.
|
All subclasses must have poll attribute with the related name "options"
|
||||||
|
|
||||||
Subclasses have to define a poll field. This must be a ForeignKeyField
|
|
||||||
to a subclass of BasePoll. There must also be a vote_class attribute
|
|
||||||
which has to be a subclass of BaseVote. Otherwise you have to override the
|
|
||||||
get_vote_class method.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vote_class: Optional[Type["BaseVote"]] = None
|
vote_class: Optional[Type["BaseVote"]] = None
|
||||||
@ -22,146 +46,135 @@ class BaseOption(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def get_votes(self):
|
@property
|
||||||
return self.get_vote_class().objects.filter(option=self)
|
def yes(self) -> Decimal:
|
||||||
|
return self.sum_weight("Y")
|
||||||
|
|
||||||
def get_vote_class(self):
|
@property
|
||||||
if self.vote_class is None:
|
def no(self) -> Decimal:
|
||||||
|
return self.sum_weight("N")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abstain(self) -> Decimal:
|
||||||
|
return self.sum_weight("A")
|
||||||
|
|
||||||
|
def sum_weight(self, value):
|
||||||
|
# We could do this in a nice .aggregate(Sum...) querystatement,
|
||||||
|
# but these might be expensive DB queries, because they are not preloaded.
|
||||||
|
# With this in-logic-counting, we operate inmemory.
|
||||||
|
weight_sum = Decimal(0)
|
||||||
|
for vote in self.votes.all():
|
||||||
|
if vote.value == value:
|
||||||
|
weight_sum += vote.weight
|
||||||
|
return weight_sum
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_vote_class(cls):
|
||||||
|
if cls.vote_class is None:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"The option class {self} has to have an attribute vote_class."
|
f"The option class {cls} has to have an attribute vote_class."
|
||||||
)
|
)
|
||||||
return self.vote_class
|
return cls.vote_class
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def get_root_rest_element(self):
|
||||||
try:
|
return self.poll.get_root_rest_element()
|
||||||
return self.get_votes().get(value=name)
|
|
||||||
except self.get_vote_class().DoesNotExist:
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
|
|
||||||
class BaseVote(models.Model):
|
|
||||||
"""
|
|
||||||
Base vote class for an option.
|
|
||||||
|
|
||||||
Subclasses have to define an option field. This must be a ForeignKeyField
|
|
||||||
to a subclass of BasePoll.
|
|
||||||
"""
|
|
||||||
|
|
||||||
weight = models.DecimalField(
|
|
||||||
default=Decimal("1"),
|
|
||||||
null=True,
|
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
|
||||||
max_digits=15,
|
|
||||||
decimal_places=6,
|
|
||||||
)
|
|
||||||
value = models.CharField(max_length=255, null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.print_weight()
|
|
||||||
|
|
||||||
def get_value(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
def print_weight(self, raw=False):
|
|
||||||
if raw:
|
|
||||||
return self.weight
|
|
||||||
try:
|
|
||||||
percent_base = self.option.poll.get_percent_base()
|
|
||||||
except AttributeError:
|
|
||||||
# The poll class is no child of CollectVotesCast
|
|
||||||
percent_base = 0
|
|
||||||
return print_value(self.weight, percent_base)
|
|
||||||
|
|
||||||
|
|
||||||
class CollectDefaultVotesMixin(models.Model):
|
|
||||||
"""
|
|
||||||
Mixin for a poll to collect the default vote values for valid votes,
|
|
||||||
invalid votes and votes cast.
|
|
||||||
"""
|
|
||||||
|
|
||||||
votesvalid = models.DecimalField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
|
||||||
max_digits=15,
|
|
||||||
decimal_places=6,
|
|
||||||
)
|
|
||||||
votesinvalid = models.DecimalField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
|
||||||
max_digits=15,
|
|
||||||
decimal_places=6,
|
|
||||||
)
|
|
||||||
votescast = models.DecimalField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
|
||||||
max_digits=15,
|
|
||||||
decimal_places=6,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def get_percent_base_choice(self):
|
|
||||||
"""
|
|
||||||
Returns one of the strings of the percent base.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError(
|
|
||||||
"You have to provide a get_percent_base_choice() method."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PublishPollMixin(models.Model):
|
|
||||||
"""
|
|
||||||
Mixin for a poll to add a flag whether the poll is published or not.
|
|
||||||
"""
|
|
||||||
|
|
||||||
published = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def set_published(self, published):
|
|
||||||
self.published = published
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
class BasePoll(models.Model):
|
class BasePoll(models.Model):
|
||||||
"""
|
option_class: Optional[Type["BaseOption"]] = None
|
||||||
Base poll class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
vote_values = ["Votes"]
|
STATE_CREATED = 1
|
||||||
|
STATE_STARTED = 2
|
||||||
|
STATE_FINISHED = 3
|
||||||
|
STATE_PUBLISHED = 4
|
||||||
|
STATES = (
|
||||||
|
(STATE_CREATED, "Created"),
|
||||||
|
(STATE_STARTED, "Started"),
|
||||||
|
(STATE_FINISHED, "Finished"),
|
||||||
|
(STATE_PUBLISHED, "Published"),
|
||||||
|
)
|
||||||
|
state = models.IntegerField(choices=STATES, default=STATE_CREATED)
|
||||||
|
|
||||||
|
TYPE_ANALOG = "analog"
|
||||||
|
TYPE_NAMED = "named"
|
||||||
|
TYPE_PSEUDOANONYMOUS = "pseudoanonymous"
|
||||||
|
TYPES = (
|
||||||
|
(TYPE_ANALOG, "Analog"),
|
||||||
|
(TYPE_NAMED, "Named"),
|
||||||
|
(TYPE_PSEUDOANONYMOUS, "Pseudoanonymous"),
|
||||||
|
)
|
||||||
|
type = models.CharField(max_length=64, blank=False, null=False, choices=TYPES)
|
||||||
|
|
||||||
|
title = models.CharField(max_length=255, blank=True, null=False)
|
||||||
|
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
||||||
|
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
||||||
|
|
||||||
|
db_votesvalid = models.DecimalField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
db_votesinvalid = models.DecimalField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
db_votescast = models.DecimalField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def has_votes(self):
|
def get_votesvalid(self):
|
||||||
"""
|
if self.type == self.TYPE_ANALOG:
|
||||||
Returns True if there are votes in the poll.
|
return self.db_votesvalid
|
||||||
"""
|
else:
|
||||||
if self.get_votes().exists():
|
return Decimal(self.count_users_voted())
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_options(self, options_data=None, skip_autoupdate=False):
|
def set_votesvalid(self, value):
|
||||||
"""
|
if self.type != self.TYPE_ANALOG:
|
||||||
Adds new option objects to the poll.
|
raise ValueError("Do not set votesvalid for non analog polls")
|
||||||
|
self.db_votesvalid = value
|
||||||
|
|
||||||
option_data: A list of arguments for the option.
|
votesvalid = property(get_votesvalid, set_votesvalid)
|
||||||
"""
|
|
||||||
if options_data is None:
|
|
||||||
options_data = []
|
|
||||||
|
|
||||||
for option_data in options_data:
|
def get_votesinvalid(self):
|
||||||
option = self.get_option_class()(**option_data)
|
if self.type == self.TYPE_ANALOG:
|
||||||
option.poll = self
|
return self.db_votesinvalid
|
||||||
option.save(skip_autoupdate=skip_autoupdate)
|
else:
|
||||||
|
return Decimal(0)
|
||||||
|
|
||||||
|
def set_votesinvalid(self, value):
|
||||||
|
if self.type != self.TYPE_ANALOG:
|
||||||
|
raise ValueError("Do not set votesinvalid for non analog polls")
|
||||||
|
self.db_votesinvalid = value
|
||||||
|
|
||||||
|
votesinvalid = property(get_votesinvalid, set_votesinvalid)
|
||||||
|
|
||||||
|
def get_votescast(self):
|
||||||
|
if self.type == self.TYPE_ANALOG:
|
||||||
|
return self.db_votescast
|
||||||
|
else:
|
||||||
|
return Decimal(self.count_users_voted())
|
||||||
|
|
||||||
|
def set_votescast(self, value):
|
||||||
|
if self.type != self.TYPE_ANALOG:
|
||||||
|
raise ValueError("Do not set votescast for non analog polls")
|
||||||
|
self.db_votescast = value
|
||||||
|
|
||||||
|
votescast = property(get_votescast, set_votescast)
|
||||||
|
|
||||||
|
def count_users_voted(self):
|
||||||
|
return self.voted.all().count()
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
"""
|
"""
|
||||||
@ -169,75 +182,49 @@ class BasePoll(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.get_option_class().objects.filter(poll=self)
|
return self.get_option_class().objects.filter(poll=self)
|
||||||
|
|
||||||
def get_option_class(self):
|
def create_options(self):
|
||||||
"""
|
""" Should be called after creation of this model. """
|
||||||
Returns the option class for the poll. Default is self.option_class.
|
raise NotImplementedError()
|
||||||
"""
|
|
||||||
return self.option_class
|
|
||||||
|
|
||||||
def get_vote_values(self):
|
@classmethod
|
||||||
"""
|
def get_option_class(cls):
|
||||||
Returns the possible values for the poll. Default is as list.
|
if cls.option_class is None:
|
||||||
"""
|
raise NotImplementedError(
|
||||||
return self.vote_values
|
f"The poll class {cls} has to have an attribute option_class."
|
||||||
|
)
|
||||||
|
return cls.option_class
|
||||||
|
|
||||||
def get_vote_class(self):
|
@classmethod
|
||||||
"""
|
def get_vote_class(cls):
|
||||||
Returns the related vote class.
|
return cls.get_option_class().get_vote_class()
|
||||||
"""
|
|
||||||
return self.get_option_class().vote_class
|
|
||||||
|
|
||||||
def get_votes(self):
|
def get_votes(self):
|
||||||
"""
|
"""
|
||||||
Return a QuerySet with all vote objects related to this poll.
|
Return a QuerySet with all vote objects related to this poll.
|
||||||
|
|
||||||
|
TODO: This might be a performance issue when used in properties that are serialized.
|
||||||
"""
|
"""
|
||||||
return self.get_vote_class().objects.filter(option__poll__id=self.id)
|
return self.get_vote_class().objects.filter(option__poll__id=self.id)
|
||||||
|
|
||||||
def set_vote_objects_with_values(self, option, data, skip_autoupdate=False):
|
def pseudoanonymize(self):
|
||||||
"""
|
for vote in self.get_votes():
|
||||||
Creates or updates the vote objects for the poll.
|
vote.user = None
|
||||||
"""
|
vote.save()
|
||||||
for value in self.get_vote_values():
|
|
||||||
try:
|
|
||||||
vote = self.get_votes().filter(option=option).get(value=value)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
vote = self.get_vote_class()(option=option, value=value)
|
|
||||||
vote.weight = data[value]
|
|
||||||
vote.save(skip_autoupdate=skip_autoupdate)
|
|
||||||
|
|
||||||
def get_vote_objects_with_values(self, option_id):
|
def reset(self):
|
||||||
"""
|
self.voted.clear()
|
||||||
Returns the vote values and their weight as a list with two elements.
|
|
||||||
"""
|
|
||||||
values = []
|
|
||||||
for value in self.get_vote_values():
|
|
||||||
try:
|
|
||||||
vote = self.get_votes().filter(option=option_id).get(value=value)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
values.append(self.get_vote_class()(value=value, weight=""))
|
|
||||||
else:
|
|
||||||
values.append(vote)
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
# Delete votes
|
||||||
|
votes = self.get_votes()
|
||||||
|
votes_id = [vote.id for vote in votes]
|
||||||
|
votes.delete()
|
||||||
|
collection = self.get_vote_class().get_collection_string()
|
||||||
|
inform_deleted_data((collection, id) for id in votes_id)
|
||||||
|
|
||||||
def print_value(value, percent_base=0):
|
# Reset state
|
||||||
"""
|
self.state = BasePoll.STATE_CREATED
|
||||||
Returns a human readable string for the vote value. It is 'majority',
|
if self.type == self.TYPE_ANALOG:
|
||||||
'undocumented' or the vote value with percent value if so.
|
self.votesvalid = None
|
||||||
"""
|
self.votesinvalid = None
|
||||||
if value == -1:
|
self.votescast = None
|
||||||
verbose_value = "majority"
|
self.save()
|
||||||
elif value == -2:
|
|
||||||
verbose_value = "undocumented"
|
|
||||||
elif value is None:
|
|
||||||
verbose_value = "undocumented"
|
|
||||||
else:
|
|
||||||
if percent_base:
|
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
|
||||||
verbose_value = "%d (%s %%)" % (
|
|
||||||
value,
|
|
||||||
locale.format("%.1f", value * percent_base),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
verbose_value = value
|
|
||||||
return verbose_value
|
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
from ..utils.rest_api import ValidationError
|
BASE_POLL_FIELDS = (
|
||||||
|
"state",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"groups",
|
||||||
|
"votesvalid",
|
||||||
|
"votesinvalid",
|
||||||
|
"votescast",
|
||||||
|
"options",
|
||||||
|
"voted",
|
||||||
|
"id",
|
||||||
|
)
|
||||||
|
|
||||||
|
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain", "votes")
|
||||||
|
|
||||||
def default_votes_validator(data):
|
BASE_VOTE_FIELDS = ("id", "weight", "value", "user")
|
||||||
"""
|
|
||||||
Use this validator in your poll serializer. It checks that the values
|
|
||||||
for the default votes (see models.CollectDefaultVotesMixin) are greater
|
|
||||||
than or equal to -2.
|
|
||||||
"""
|
|
||||||
for key in data:
|
|
||||||
if (
|
|
||||||
key in ("votesvalid", "votesinvalid", "votescast")
|
|
||||||
and data[key] is not None
|
|
||||||
and data[key] < -2
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "Value for {0} must not be less than -2", "args": [key]}
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
165
openslides/poll/views.py
Normal file
165
openslides/poll/views.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
from openslides.utils.auth import in_some_groups
|
||||||
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
|
from openslides.utils.rest_api import (
|
||||||
|
DecimalField,
|
||||||
|
GenericViewSet,
|
||||||
|
ListModelMixin,
|
||||||
|
ModelViewSet,
|
||||||
|
Response,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
ValidationError,
|
||||||
|
detail_route,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .models import BasePoll
|
||||||
|
|
||||||
|
|
||||||
|
class BasePollViewSet(ModelViewSet):
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
the vote view is checked seperately. For all other views manage permissions
|
||||||
|
are required.
|
||||||
|
"""
|
||||||
|
if self.action == "vote":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.has_manage_permissions()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
poll = serializer.save()
|
||||||
|
poll.create_options()
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Customized view endpoint to update a motion poll.
|
||||||
|
"""
|
||||||
|
poll = self.get_object()
|
||||||
|
|
||||||
|
if poll.state != BasePoll.STATE_CREATED:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You can just edit a poll if it was not started."}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().update(*args, **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def start(self, request, pk):
|
||||||
|
poll = self.get_object()
|
||||||
|
if poll.state != BasePoll.STATE_CREATED:
|
||||||
|
raise ValidationError({"detail": "Wrong poll state"})
|
||||||
|
poll.state = BasePoll.STATE_STARTED
|
||||||
|
|
||||||
|
poll.save()
|
||||||
|
inform_changed_data(poll.get_votes())
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def stop(self, request, pk):
|
||||||
|
poll = self.get_object()
|
||||||
|
if poll.state != BasePoll.STATE_STARTED:
|
||||||
|
raise ValidationError({"detail": "Wrong poll state"})
|
||||||
|
|
||||||
|
poll.state = BasePoll.STATE_FINISHED
|
||||||
|
poll.save()
|
||||||
|
inform_changed_data(poll.get_votes())
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def publish(self, request, pk):
|
||||||
|
poll = self.get_object()
|
||||||
|
if poll.state != BasePoll.STATE_FINISHED:
|
||||||
|
raise ValidationError({"detail": "Wrong poll state"})
|
||||||
|
|
||||||
|
poll.state = BasePoll.STATE_PUBLISHED
|
||||||
|
poll.save()
|
||||||
|
inform_changed_data(poll.get_votes())
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def pseudoanonymize(self, request, pk):
|
||||||
|
poll = self.get_object()
|
||||||
|
|
||||||
|
if poll.state not in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Pseudoanonmizing can only be done after a finished poll"}
|
||||||
|
)
|
||||||
|
if poll.type != BasePoll.TYPE_NAMED:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You can just pseudoanonymize named polls"}
|
||||||
|
)
|
||||||
|
|
||||||
|
poll.pseudoanonymize()
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def reset(self, request, pk):
|
||||||
|
poll = self.get_object()
|
||||||
|
|
||||||
|
if poll.state not in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You can only reset this poll after it is finished"}
|
||||||
|
)
|
||||||
|
|
||||||
|
poll.reset()
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def vote(self, request, pk):
|
||||||
|
"""
|
||||||
|
For motion polls: Just "Y", "N" or "A" (if pollmethod is "YNA")
|
||||||
|
"""
|
||||||
|
poll = self.get_object()
|
||||||
|
if poll.state != BasePoll.STATE_STARTED:
|
||||||
|
raise ValidationError({"detail": "You cannot vote for an unstarted poll"})
|
||||||
|
|
||||||
|
if isinstance(request.user, AnonymousUser):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
# check permissions based on poll type and handle requests
|
||||||
|
if poll.type == BasePoll.TYPE_ANALOG:
|
||||||
|
if not self.has_manage_permissions():
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
self.handle_analog_vote(request.data, poll, request.user)
|
||||||
|
# special: change the poll state to finished.
|
||||||
|
poll.state = BasePoll.STATE_FINISHED
|
||||||
|
poll.save()
|
||||||
|
|
||||||
|
elif poll.type == BasePoll.TYPE_NAMED:
|
||||||
|
self.assert_can_vote(poll, request)
|
||||||
|
self.handle_named_vote(request.data, poll, request.user)
|
||||||
|
poll.voted.add(request.user)
|
||||||
|
|
||||||
|
elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
|
||||||
|
self.assert_can_vote(poll, request)
|
||||||
|
|
||||||
|
if request.user in poll.voted.all():
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You have already voted for this poll."}
|
||||||
|
)
|
||||||
|
self.handle_pseudoanonymous_vote(request.data, poll)
|
||||||
|
poll.voted.add(request.user)
|
||||||
|
|
||||||
|
inform_changed_data(poll) # needed for the changed voted relation
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
def assert_can_vote(self, poll, request):
|
||||||
|
"""
|
||||||
|
Raises a permission denied, if the user is not in a poll group
|
||||||
|
and present
|
||||||
|
"""
|
||||||
|
if not request.user.is_present or not in_some_groups(
|
||||||
|
request.user.id, poll.groups.all(), exact=True
|
||||||
|
):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
def parse_decimal_value(self, value, min_value=None):
|
||||||
|
""" Raises a ValidationError on incorrect values """
|
||||||
|
field = DecimalField(min_value=min_value, max_digits=15, decimal_places=6)
|
||||||
|
return field.to_internal_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVoteViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
|
pass
|
@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from .cache import element_cache
|
from .cache import element_cache
|
||||||
|
|
||||||
@ -111,28 +112,40 @@ async def async_has_perm(user_id: int, perm: str) -> bool:
|
|||||||
return has_perm
|
return has_perm
|
||||||
|
|
||||||
|
|
||||||
def in_some_groups(user_id: int, groups: List[int]) -> bool:
|
def in_some_groups(
|
||||||
|
user_id: int, groups: Union[List[int], QuerySet], exact: bool = False
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks that user is in at least one given group. Groups can be given as a list
|
Checks that user is in at least one given group. Groups can be given as a list
|
||||||
of ids or group instances. If the user is in the admin group (pk = 2) the result
|
of ids or a QuerySet.
|
||||||
is always true, even if no groups are given.
|
|
||||||
|
If exact is false (default) and the user is in the admin group (pk = 2),
|
||||||
|
the result is always true, even if no groups are given.
|
||||||
|
|
||||||
|
If exact is true, the user must be in one of the groups, ignoring the possible
|
||||||
|
superadmin-status of the user.
|
||||||
|
|
||||||
user_id 0 means anonymous user.
|
user_id 0 means anonymous user.
|
||||||
"""
|
"""
|
||||||
# Convert user to right type
|
# Convert user to right type
|
||||||
# TODO: Remove this and make use, that user has always the right type
|
# TODO: Remove this and make use, that user has always the right type
|
||||||
user_id = user_to_user_id(user_id)
|
user_id = user_to_user_id(user_id)
|
||||||
return async_to_sync(async_in_some_groups)(user_id, groups)
|
return async_to_sync(async_in_some_groups)(user_id, groups, exact)
|
||||||
|
|
||||||
|
|
||||||
async def async_in_some_groups(user_id: int, groups: List[int]) -> bool:
|
async def async_in_some_groups(
|
||||||
|
user_id: int, groups: Union[List[int], QuerySet], exact: bool = False
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks that user is in at least one given group. Groups can be given as a list
|
Checks that user is in at least one given group. Groups can be given as a list
|
||||||
of ids. If the user is in the admin group (pk = 2) the result
|
of ids or a QuerySet. If the user is in the admin group (pk = 2) the result
|
||||||
is always true, even if no groups are given.
|
is always true, even if no groups are given.
|
||||||
|
|
||||||
user_id 0 means anonymous user.
|
user_id 0 means anonymous user.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(groups, QuerySet):
|
||||||
|
groups = [group.pk for group in groups]
|
||||||
|
|
||||||
if not user_id and not await async_anonymous_is_enabled():
|
if not user_id and not await async_anonymous_is_enabled():
|
||||||
in_some_groups = False
|
in_some_groups = False
|
||||||
elif not user_id:
|
elif not user_id:
|
||||||
@ -144,7 +157,7 @@ async def async_in_some_groups(user_id: int, groups: List[int]) -> bool:
|
|||||||
)
|
)
|
||||||
if user_data is None:
|
if user_data is None:
|
||||||
raise UserDoesNotExist()
|
raise UserDoesNotExist()
|
||||||
if GROUP_ADMIN_PK in user_data["groups_id"]:
|
if not exact and GROUP_ADMIN_PK in user_data["groups_id"]:
|
||||||
# User in admin group (pk 2) grants all permissions.
|
# User in admin group (pk 2) grants all permissions.
|
||||||
in_some_groups = True
|
in_some_groups = True
|
||||||
else:
|
else:
|
||||||
|
@ -28,13 +28,18 @@ class Element(ElementBase, total=False):
|
|||||||
if full_data is None, it means, that the element was deleted. If reload is
|
if full_data is None, it means, that the element was deleted. If reload is
|
||||||
True, full_data is ignored and reloaded from the database later in the
|
True, full_data is ignored and reloaded from the database later in the
|
||||||
process.
|
process.
|
||||||
|
|
||||||
|
no_delete_on_restriction is a flag, which is saved into the models in the cache
|
||||||
|
as the _no_delete_on_restriction key. If this is true, there should neither be an
|
||||||
|
entry for one specific model in the changed *nor the deleted* part of the
|
||||||
|
autoupdate, if the model was restricted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
information: List[str]
|
information: List[str]
|
||||||
restricted: bool
|
|
||||||
user_id: Optional[int]
|
user_id: Optional[int]
|
||||||
disable_history: bool
|
disable_history: bool
|
||||||
reload: bool
|
reload: bool
|
||||||
|
no_delete_on_restriction: bool
|
||||||
|
|
||||||
|
|
||||||
AutoupdateFormat = TypedDict(
|
AutoupdateFormat = TypedDict(
|
||||||
@ -53,7 +58,7 @@ def inform_changed_data(
|
|||||||
instances: Union[Iterable[Model], Model],
|
instances: Union[Iterable[Model], Model],
|
||||||
information: List[str] = None,
|
information: List[str] = None,
|
||||||
user_id: Optional[int] = None,
|
user_id: Optional[int] = None,
|
||||||
restricted: bool = False,
|
no_delete_on_restriction: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system and the caching system about the creation or
|
Informs the autoupdate system and the caching system about the creation or
|
||||||
@ -84,8 +89,8 @@ def inform_changed_data(
|
|||||||
collection_string=root_instance.get_collection_string(),
|
collection_string=root_instance.get_collection_string(),
|
||||||
full_data=root_instance.get_full_data(),
|
full_data=root_instance.get_full_data(),
|
||||||
information=information,
|
information=information,
|
||||||
restricted=restricted,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
no_delete_on_restriction=no_delete_on_restriction,
|
||||||
)
|
)
|
||||||
|
|
||||||
bundle = autoupdate_bundle.get(threading.get_ident())
|
bundle = autoupdate_bundle.get(threading.get_ident())
|
||||||
@ -101,7 +106,6 @@ def inform_deleted_data(
|
|||||||
deleted_elements: Iterable[Tuple[str, int]],
|
deleted_elements: Iterable[Tuple[str, int]],
|
||||||
information: List[str] = None,
|
information: List[str] = None,
|
||||||
user_id: Optional[int] = None,
|
user_id: Optional[int] = None,
|
||||||
restricted: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system and the caching system about the deletion of
|
Informs the autoupdate system and the caching system about the deletion of
|
||||||
@ -119,7 +123,6 @@ def inform_deleted_data(
|
|||||||
collection_string=deleted_element[0],
|
collection_string=deleted_element[0],
|
||||||
full_data=None,
|
full_data=None,
|
||||||
information=information,
|
information=information,
|
||||||
restricted=restricted,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -197,7 +200,12 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
|||||||
cache_elements: Dict[str, Optional[Dict[str, Any]]] = {}
|
cache_elements: Dict[str, Optional[Dict[str, Any]]] = {}
|
||||||
for element in elements:
|
for element in elements:
|
||||||
element_id = get_element_id(element["collection_string"], element["id"])
|
element_id = get_element_id(element["collection_string"], element["id"])
|
||||||
cache_elements[element_id] = element["full_data"]
|
full_data = element["full_data"]
|
||||||
|
if full_data:
|
||||||
|
full_data["_no_delete_on_restriction"] = element.get(
|
||||||
|
"no_delete_on_restriction", False
|
||||||
|
)
|
||||||
|
cache_elements[element_id] = full_data
|
||||||
return await element_cache.change_elements(cache_elements)
|
return await element_cache.change_elements(cache_elements)
|
||||||
|
|
||||||
async def async_handle_collection_elements(elements: Iterable[Element]) -> None:
|
async def async_handle_collection_elements(elements: Iterable[Element]) -> None:
|
||||||
|
@ -204,7 +204,11 @@ class ElementCache:
|
|||||||
all_data: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
all_data: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||||
for element_id, data in (await self.cache_provider.get_all_data()).items():
|
for element_id, data in (await self.cache_provider.get_all_data()).items():
|
||||||
collection_string, _ = split_element_id(element_id)
|
collection_string, _ = split_element_id(element_id)
|
||||||
all_data[collection_string].append(json.loads(data.decode()))
|
element = json.loads(data.decode())
|
||||||
|
element.pop(
|
||||||
|
"_no_delete_on_restriction", False
|
||||||
|
) # remove special field for get_data_since
|
||||||
|
all_data[collection_string].append(element)
|
||||||
|
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
for collection_string in all_data.keys():
|
for collection_string in all_data.keys():
|
||||||
@ -226,7 +230,11 @@ class ElementCache:
|
|||||||
all_data: Dict[str, Dict[int, Dict[str, Any]]] = defaultdict(dict)
|
all_data: Dict[str, Dict[int, Dict[str, Any]]] = defaultdict(dict)
|
||||||
for element_id, data in (await self.cache_provider.get_all_data()).items():
|
for element_id, data in (await self.cache_provider.get_all_data()).items():
|
||||||
collection_string, id = split_element_id(element_id)
|
collection_string, id = split_element_id(element_id)
|
||||||
all_data[collection_string][id] = json.loads(data.decode())
|
element = json.loads(data.decode())
|
||||||
|
element.pop(
|
||||||
|
"_no_delete_on_restriction", False
|
||||||
|
) # remove special field for get_data_since
|
||||||
|
all_data[collection_string][id] = element
|
||||||
return dict(all_data)
|
return dict(all_data)
|
||||||
|
|
||||||
async def get_collection_data(
|
async def get_collection_data(
|
||||||
@ -241,6 +249,9 @@ class ElementCache:
|
|||||||
collection_data = {}
|
collection_data = {}
|
||||||
for id in encoded_collection_data.keys():
|
for id in encoded_collection_data.keys():
|
||||||
collection_data[id] = json.loads(encoded_collection_data[id].decode())
|
collection_data[id] = json.loads(encoded_collection_data[id].decode())
|
||||||
|
collection_data[id].pop(
|
||||||
|
"_no_delete_on_restriction", False
|
||||||
|
) # remove special field for get_data_since
|
||||||
return collection_data
|
return collection_data
|
||||||
|
|
||||||
async def get_element_data(
|
async def get_element_data(
|
||||||
@ -257,6 +268,9 @@ class ElementCache:
|
|||||||
if encoded_element is None:
|
if encoded_element is None:
|
||||||
return None
|
return None
|
||||||
element = json.loads(encoded_element.decode()) # type: ignore
|
element = json.loads(encoded_element.decode()) # type: ignore
|
||||||
|
element.pop(
|
||||||
|
"_no_delete_on_restriction", False
|
||||||
|
) # remove special field for get_data_since
|
||||||
|
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
element = await self.restrict_element_data(
|
element = await self.restrict_element_data(
|
||||||
@ -319,6 +333,16 @@ class ElementCache:
|
|||||||
# the list(...) is important, because `changed_elements` will be
|
# the list(...) is important, because `changed_elements` will be
|
||||||
# altered during iteration and restricting data
|
# altered during iteration and restricting data
|
||||||
for collection_string, elements in list(changed_elements.items()):
|
for collection_string, elements in list(changed_elements.items()):
|
||||||
|
# Remove the _no_delete_on_restriction from each element. Collect all ids, where
|
||||||
|
# this field is absent or False.
|
||||||
|
unrestricted_ids = set()
|
||||||
|
for element in elements:
|
||||||
|
no_delete_on_restriction = element.pop(
|
||||||
|
"_no_delete_on_restriction", False
|
||||||
|
)
|
||||||
|
if not no_delete_on_restriction:
|
||||||
|
unrestricted_ids.add(element["id"])
|
||||||
|
|
||||||
cacheable = self.cachables[collection_string]
|
cacheable = self.cachables[collection_string]
|
||||||
restricted_elements = await cacheable.restrict_elements(
|
restricted_elements = await cacheable.restrict_elements(
|
||||||
user_id, elements
|
user_id, elements
|
||||||
@ -327,11 +351,12 @@ class ElementCache:
|
|||||||
# If the model is personalized, it must not be deleted for other users
|
# If the model is personalized, it must not be deleted for other users
|
||||||
if not cacheable.personalized_model:
|
if not cacheable.personalized_model:
|
||||||
# Add removed objects (through restricter) to deleted elements.
|
# Add removed objects (through restricter) to deleted elements.
|
||||||
element_ids = set([element["id"] for element in elements])
|
|
||||||
restricted_element_ids = set(
|
restricted_element_ids = set(
|
||||||
[element["id"] for element in restricted_elements]
|
[element["id"] for element in restricted_elements]
|
||||||
)
|
)
|
||||||
for id in element_ids - restricted_element_ids:
|
# Delete all ids, that are allowed to be deleted (see unrestricted_ids) and are
|
||||||
|
# not present after restricting the data.
|
||||||
|
for id in unrestricted_ids - restricted_element_ids:
|
||||||
deleted_elements.append(get_element_id(collection_string, id))
|
deleted_elements.append(get_element_id(collection_string, id))
|
||||||
|
|
||||||
if not restricted_elements:
|
if not restricted_elements:
|
||||||
|
@ -8,7 +8,7 @@ from . import logging
|
|||||||
from .access_permissions import BaseAccessPermissions
|
from .access_permissions import BaseAccessPermissions
|
||||||
from .autoupdate import Element, inform_changed_data, inform_changed_elements
|
from .autoupdate import Element, inform_changed_data, inform_changed_elements
|
||||||
from .rest_api import model_serializer_classes
|
from .rest_api import model_serializer_classes
|
||||||
from .utils import convert_camel_case_to_pseudo_snake_case
|
from .utils import convert_camel_case_to_pseudo_snake_case, get_element_id
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -90,7 +90,16 @@ class RESTModelMixin:
|
|||||||
"""
|
"""
|
||||||
return self.pk # type: ignore
|
return self.pk # type: ignore
|
||||||
|
|
||||||
def save(self, skip_autoupdate: bool = False, *args: Any, **kwargs: Any) -> Any:
|
def get_element_id(self) -> str:
|
||||||
|
return get_element_id(self.get_collection_string(), self.get_rest_pk())
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self,
|
||||||
|
skip_autoupdate: bool = False,
|
||||||
|
no_delete_on_restriction: bool = False,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Calls Django's save() method and afterwards hits the autoupdate system.
|
Calls Django's save() method and afterwards hits the autoupdate system.
|
||||||
|
|
||||||
@ -104,7 +113,10 @@ class RESTModelMixin:
|
|||||||
|
|
||||||
return_value = super().save(*args, **kwargs) # type: ignore
|
return_value = super().save(*args, **kwargs) # type: ignore
|
||||||
if not skip_autoupdate:
|
if not skip_autoupdate:
|
||||||
inform_changed_data(self.get_root_rest_element())
|
inform_changed_data(
|
||||||
|
self.get_root_rest_element(),
|
||||||
|
no_delete_on_restriction=no_delete_on_restriction,
|
||||||
|
)
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
def delete(self, skip_autoupdate: bool = False, *args: Any, **kwargs: Any) -> Any:
|
def delete(self, skip_autoupdate: bool = False, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
from django.test import TestCase as _TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(_TestCase):
|
|
||||||
"""
|
|
||||||
Does currently nothing.
|
|
||||||
"""
|
|
@ -1,7 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
from typing import Dict, Generator, Optional, Tuple, Type, Union
|
from typing import Any, Dict, Generator, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import roman
|
import roman
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -64,6 +64,14 @@ def str_dict_to_bytes(str_dict: Dict[str, str]) -> Dict[bytes, bytes]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def is_int(obj: Any) -> bool:
|
||||||
|
try:
|
||||||
|
int(obj)
|
||||||
|
return True
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
_models_to_collection_string: Dict[str, Type[Model]] = {}
|
_models_to_collection_string: Dict[str, Type[Model]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,4 +15,4 @@ PyPDF2>=1.26,<1.27
|
|||||||
roman>=2.0,<3.2
|
roman>=2.0,<3.2
|
||||||
setuptools>=29.0,<42.0
|
setuptools>=29.0,<42.0
|
||||||
typing_extensions>=3.6.6,<3.8
|
typing_extensions>=3.6.6,<3.8
|
||||||
websockets>=8.0,<9.0
|
websockets>=8.0,<9.0
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestItemManager(TestCase):
|
class TestItemManager(TestCase):
|
||||||
|
@ -16,7 +16,7 @@ from openslides.motions.models import Motion
|
|||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import Group
|
from openslides.users.models import Group
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ...common_groups import GROUP_DEFAULT_PK
|
from ...common_groups import GROUP_DEFAULT_PK
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
@ -284,25 +284,11 @@ class ManageSpeaker(TestCase):
|
|||||||
Tests managing speakers.
|
Tests managing speakers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def advancedSetUp(self):
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
|
|
||||||
self.list_of_speakers = Topic.objects.create(
|
self.list_of_speakers = Topic.objects.create(
|
||||||
title="test_title_aZaedij4gohn5eeQu8fe"
|
title="test_title_aZaedij4gohn5eeQu8fe"
|
||||||
).list_of_speakers
|
).list_of_speakers
|
||||||
self.user = get_user_model().objects.create_user(
|
self.user, _ = self.create_user()
|
||||||
username="test_user_jooSaex1bo5ooPhuphae",
|
|
||||||
password="test_password_e6paev4zeeh9n",
|
|
||||||
)
|
|
||||||
|
|
||||||
def revoke_admin_rights(self):
|
|
||||||
admin = get_user_model().objects.get(username="admin")
|
|
||||||
group_admin = admin.groups.get(name="Admin")
|
|
||||||
group_delegates = type(group_admin).objects.get(name="Delegates")
|
|
||||||
admin.groups.add(group_delegates)
|
|
||||||
admin.groups.remove(group_admin)
|
|
||||||
inform_changed_data(admin)
|
|
||||||
|
|
||||||
def test_add_oneself_once(self):
|
def test_add_oneself_once(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -383,7 +369,7 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_add_someone_else_non_admin(self):
|
def test_add_someone_else_non_admin(self):
|
||||||
self.revoke_admin_rights()
|
self.make_admin_delegate()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
@ -392,6 +378,7 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_remove_someone_else(self):
|
def test_remove_someone_else(self):
|
||||||
|
print(self.user)
|
||||||
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
@ -419,7 +406,7 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_remove_someone_else_non_admin(self):
|
def test_remove_someone_else_non_admin(self):
|
||||||
self.revoke_admin_rights()
|
self.make_admin_delegate()
|
||||||
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
@ -433,14 +420,13 @@ class ManageSpeaker(TestCase):
|
|||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
{"user": self.user.pk, "marked": True},
|
{"user": self.user.pk, "marked": True},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(Speaker.objects.get().marked)
|
self.assertTrue(Speaker.objects.get().marked)
|
||||||
|
|
||||||
def test_mark_speaker_non_admin(self):
|
def test_mark_speaker_non_admin(self):
|
||||||
self.revoke_admin_rights()
|
self.make_admin_delegate()
|
||||||
Speaker.objects.add(self.user, self.list_of_speakers)
|
Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
@ -515,7 +501,7 @@ class ManageSpeaker(TestCase):
|
|||||||
|
|
||||||
def test_readd_last_speaker_no_admin(self):
|
def test_readd_last_speaker_no_admin(self):
|
||||||
self.util_add_user_as_last_speaker()
|
self.util_add_user_as_last_speaker()
|
||||||
self.revoke_admin_rights()
|
self.make_admin_delegate()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
|
507
tests/integration/assignments/test_polls.py
Normal file
507
tests/integration/assignments/test_polls.py
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from openslides.assignments.models import (
|
||||||
|
Assignment,
|
||||||
|
AssignmentOption,
|
||||||
|
AssignmentPoll,
|
||||||
|
AssignmentVote,
|
||||||
|
)
|
||||||
|
from openslides.poll.models import BasePoll
|
||||||
|
from openslides.utils.auth import get_group_model
|
||||||
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAssignmentPoll(TestCase):
|
||||||
|
def advancedSetUp(self):
|
||||||
|
self.assignment = Assignment.objects.create(
|
||||||
|
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
|
||||||
|
)
|
||||||
|
self.assignment.add_candidate(self.admin)
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_ailai4toogh3eefaa2Vo",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertTrue(AssignmentPoll.objects.exists())
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
|
||||||
|
self.assertEqual(poll.pollmethod, "YNA")
|
||||||
|
self.assertEqual(poll.type, "named")
|
||||||
|
# Check defaults
|
||||||
|
self.assertTrue(poll.global_no)
|
||||||
|
self.assertTrue(poll.global_abstain)
|
||||||
|
self.assertFalse(poll.allow_multiple_votes_per_candidate)
|
||||||
|
self.assertEqual(poll.votes_amount, 1)
|
||||||
|
self.assertEqual(poll.assignment.id, self.assignment.id)
|
||||||
|
self.assertTrue(poll.options.exists())
|
||||||
|
option = AssignmentOption.objects.get()
|
||||||
|
self.assertTrue(option.user.id, self.admin.id)
|
||||||
|
|
||||||
|
def test_all_fields(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_ahThai4pae1pi4xoogoo",
|
||||||
|
"pollmethod": "YN",
|
||||||
|
"type": "pseudoanonymous",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
"global_no": False,
|
||||||
|
"global_abstain": False,
|
||||||
|
"allow_multiple_votes_per_candidate": True,
|
||||||
|
"votes_amount": 5,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertTrue(AssignmentPoll.objects.exists())
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.title, "test_title_ahThai4pae1pi4xoogoo")
|
||||||
|
self.assertEqual(poll.pollmethod, "YN")
|
||||||
|
self.assertEqual(poll.type, "pseudoanonymous")
|
||||||
|
self.assertFalse(poll.global_no)
|
||||||
|
self.assertFalse(poll.global_abstain)
|
||||||
|
self.assertTrue(poll.allow_multiple_votes_per_candidate)
|
||||||
|
self.assertEqual(poll.votes_amount, 5)
|
||||||
|
|
||||||
|
def test_no_candidates(self):
|
||||||
|
self.assignment.remove_candidate(self.admin)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_eing5eipue5cha2Iefai",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_title(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{"pollmethod": "YNA", "type": "named", "assignment_id": self.assignment.id},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_pollmethod(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_OoCh9aitaeyaeth8nom1",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_type(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Ail9Eizohshim0fora6o",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_assignment_id(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_eic7ooxaht5mee3quohK",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "named",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_with_groups(self):
|
||||||
|
group1 = get_group_model().objects.get(pk=1)
|
||||||
|
group2 = get_group_model().objects.get(pk=2)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Thoo2eiphohhi1eeXoow",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
"groups_id": [1, 2],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertTrue(group1 in poll.groups.all())
|
||||||
|
self.assertTrue(group2 in poll.groups.all())
|
||||||
|
|
||||||
|
def test_with_empty_groups(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Thoo2eiphohhi1eeXoow",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
"groups_id": [],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertFalse(poll.groups.exists())
|
||||||
|
|
||||||
|
def test_not_supported_type(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_yaiyeighoh0Iraet3Ahc",
|
||||||
|
"pollmethod": "YNA",
|
||||||
|
"type": "not_existing",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
def test_not_supported_pollmethod(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_SeVaiteYeiNgie5Xoov8",
|
||||||
|
"pollmethod": "not_existing",
|
||||||
|
"type": "named",
|
||||||
|
"assignment_id": self.assignment.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.exists())
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAssignmentPoll(TestCase):
|
||||||
|
"""
|
||||||
|
Tests updating polls of assignments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def advancedSetUp(self):
|
||||||
|
self.assignment = Assignment.objects.create(
|
||||||
|
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
|
||||||
|
)
|
||||||
|
self.assignment.add_candidate(self.admin)
|
||||||
|
self.group = get_group_model().objects.get(pk=1)
|
||||||
|
self.poll = AssignmentPoll.objects.create(
|
||||||
|
assignment=self.assignment,
|
||||||
|
title="test_title_beeFaihuNae1vej2ai8m",
|
||||||
|
pollmethod="votes",
|
||||||
|
type=BasePoll.TYPE_NAMED,
|
||||||
|
)
|
||||||
|
self.poll.create_options()
|
||||||
|
self.poll.groups.add(self.group)
|
||||||
|
|
||||||
|
def test_patch_title(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"title": "test_title_Aishohh1ohd0aiSut7gi"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.title, "test_title_Aishohh1ohd0aiSut7gi")
|
||||||
|
|
||||||
|
def test_prevent_patching_assignment(self):
|
||||||
|
assignment = Assignment(title="test_title_phohdah8quukooHeetuz", open_posts=1)
|
||||||
|
assignment.save()
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"assignment_id": assignment.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.assignment.id, self.assignment.id) # unchanged
|
||||||
|
|
||||||
|
def test_patch_pollmethod(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]), {"pollmethod": "YNA"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.pollmethod, "YNA")
|
||||||
|
|
||||||
|
def test_patch_invalid_pollmethod(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"pollmethod": "invalid"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.pollmethod, "votes")
|
||||||
|
|
||||||
|
def test_patch_type(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]), {"type": "analog"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.type, "analog")
|
||||||
|
|
||||||
|
def test_patch_invalid_type(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]), {"type": "invalid"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.type, "named")
|
||||||
|
|
||||||
|
def test_patch_groups_to_empty(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"groups_id": []},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertFalse(poll.groups.exists())
|
||||||
|
|
||||||
|
def test_patch_groups(self):
|
||||||
|
group2 = get_group_model().objects.get(pk=2)
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"groups_id": [group2.id]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.groups.count(), 1)
|
||||||
|
self.assertEqual(poll.groups.get(), group2)
|
||||||
|
|
||||||
|
def test_patch_wrong_state(self):
|
||||||
|
self.poll.state = 2
|
||||||
|
self.poll.save()
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"title": "test_title_Oophah8EaLaequu3toh8"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.title, "test_title_beeFaihuNae1vej2ai8m")
|
||||||
|
|
||||||
|
def test_patch_multiple_fields(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
||||||
|
{
|
||||||
|
"title": "test_title_ees6Tho8ahheen4cieja",
|
||||||
|
"pollmethod": "votes",
|
||||||
|
"global_no": True,
|
||||||
|
"global_abstain": False,
|
||||||
|
"allow_multiple_votes_per_candidate": True,
|
||||||
|
"votes_amount": 42,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.title, "test_title_ees6Tho8ahheen4cieja")
|
||||||
|
self.assertEqual(poll.pollmethod, "votes")
|
||||||
|
self.assertTrue(poll.global_no)
|
||||||
|
self.assertFalse(poll.global_abstain)
|
||||||
|
self.assertTrue(poll.allow_multiple_votes_per_candidate)
|
||||||
|
self.assertEqual(poll.votes_amount, 42)
|
||||||
|
|
||||||
|
|
||||||
|
class VoteAssignmentPollAnalogYNA(TestCase):
|
||||||
|
def advancedSetUp(self):
|
||||||
|
self.assignment = Assignment.objects.create(
|
||||||
|
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
|
||||||
|
)
|
||||||
|
self.assignment.add_candidate(self.admin)
|
||||||
|
self.poll = AssignmentPoll.objects.create(
|
||||||
|
assignment=self.assignment,
|
||||||
|
title="test_title_beeFaihuNae1vej2ai8m",
|
||||||
|
pollmethod="YNA",
|
||||||
|
type=BasePoll.TYPE_ANALOG,
|
||||||
|
)
|
||||||
|
self.poll.create_options()
|
||||||
|
|
||||||
|
def start_poll(self):
|
||||||
|
self.poll.state = AssignmentPoll.STATE_STARTED
|
||||||
|
self.poll.save()
|
||||||
|
|
||||||
|
def add_second_candidate(self):
|
||||||
|
user, _ = self.create_user()
|
||||||
|
AssignmentOption.objects.create(user=user, poll=self.poll)
|
||||||
|
|
||||||
|
def test_start_poll(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-start", args=[self.poll.pk])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
|
self.assertEqual(poll.votesvalid, None)
|
||||||
|
self.assertEqual(poll.votesinvalid, None)
|
||||||
|
self.assertEqual(poll.votescast, None)
|
||||||
|
self.assertFalse(poll.get_votes().exists())
|
||||||
|
|
||||||
|
def test_vote(self):
|
||||||
|
self.add_second_candidate()
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||||
|
"2": {"Y": "30", "N": "-2", "A": "8.93"},
|
||||||
|
},
|
||||||
|
"votesvalid": "4.64",
|
||||||
|
"votesinvalid": "-2",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(AssignmentVote.objects.count(), 6)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||||
|
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||||
|
self.assertEqual(poll.votescast, None)
|
||||||
|
self.assertEqual(poll.state, AssignmentPoll.STATE_FINISHED)
|
||||||
|
option1 = poll.options.get(pk=1)
|
||||||
|
option2 = poll.options.get(pk=2)
|
||||||
|
self.assertEqual(option1.yes, Decimal("1"))
|
||||||
|
self.assertEqual(option1.no, Decimal("2.35"))
|
||||||
|
self.assertEqual(option1.abstain, Decimal("-1"))
|
||||||
|
self.assertEqual(option2.yes, Decimal("30"))
|
||||||
|
self.assertEqual(option2.no, Decimal("-2"))
|
||||||
|
self.assertEqual(option2.abstain, Decimal("8.93"))
|
||||||
|
|
||||||
|
def test_too_many_options(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||||
|
"2": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
|
def test_too_few_options(self):
|
||||||
|
self.add_second_candidate()
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
|
def test_wrong_options(self):
|
||||||
|
user, _ = self.create_user()
|
||||||
|
self.assignment.add_candidate(user)
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||||
|
"2": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
|
def test_no_permissions(self):
|
||||||
|
self.start_poll()
|
||||||
|
self.make_admin_delegate()
|
||||||
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_wrong_state(self):
|
||||||
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_data(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_wrong_data_format(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
[1, 2, 5],
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_wrong_option_format(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"options": [1, "string"]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
|
def test_wrong_option_id_type(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"options": {"string": "some_other_string"}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_wrong_vote_data(self):
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"options": {"1": [None]}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_missing_vote_value(self):
|
||||||
|
self.start_poll()
|
||||||
|
for value in "YNA":
|
||||||
|
data = {"options": {"1": {"Y": "1", "N": "3", "A": "-1"}}}
|
||||||
|
del data["options"]["1"][value]
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]), data, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(AssignmentVote.objects.exists())
|
@ -9,7 +9,7 @@ from openslides.assignments.models import Assignment
|
|||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class CreateAssignment(TestCase):
|
|||||||
self.assertTrue(assignment.attachments.exists())
|
self.assertTrue(assignment.attachments.exists())
|
||||||
|
|
||||||
|
|
||||||
class CanidatureSelf(TestCase):
|
class CandidatureSelf(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests self candidation view.
|
Tests self candidation view.
|
||||||
"""
|
"""
|
||||||
@ -99,7 +99,7 @@ class CanidatureSelf(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_nominate_self_twice(self):
|
def test_nominate_self_twice(self):
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
self.assignment.add_candidate(get_user_model().objects.get(username="admin"))
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("assignment-candidature-self", args=[self.assignment.pk])
|
reverse("assignment-candidature-self", args=[self.assignment.pk])
|
||||||
@ -152,7 +152,7 @@ class CanidatureSelf(TestCase):
|
|||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_withdraw_self(self):
|
def test_withdraw_self(self):
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
self.assignment.add_candidate(get_user_model().objects.get(username="admin"))
|
||||||
|
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse("assignment-candidature-self", args=[self.assignment.pk])
|
reverse("assignment-candidature-self", args=[self.assignment.pk])
|
||||||
@ -173,7 +173,7 @@ class CanidatureSelf(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_withdraw_self_when_finished(self):
|
def test_withdraw_self_when_finished(self):
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
self.assignment.add_candidate(get_user_model().objects.get(username="admin"))
|
||||||
self.assignment.set_phase(Assignment.PHASE_FINISHED)
|
self.assignment.set_phase(Assignment.PHASE_FINISHED)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
|
|
||||||
@ -184,7 +184,7 @@ class CanidatureSelf(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_withdraw_self_during_voting(self):
|
def test_withdraw_self_during_voting(self):
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
self.assignment.add_candidate(get_user_model().objects.get(username="admin"))
|
||||||
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ class CanidatureSelf(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_withdraw_self_during_voting_non_admin(self):
|
def test_withdraw_self_during_voting_non_admin(self):
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
self.assignment.add_candidate(get_user_model().objects.get(username="admin"))
|
||||||
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
admin = get_user_model().objects.get(username="admin")
|
admin = get_user_model().objects.get(username="admin")
|
||||||
@ -267,7 +267,7 @@ class CandidatureOther(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_nominate_other_twice(self):
|
def test_nominate_other_twice(self):
|
||||||
self.assignment.set_candidate(
|
self.assignment.add_candidate(
|
||||||
get_user_model().objects.get(username="test_user_eeheekai4Phue6cahtho")
|
get_user_model().objects.get(username="test_user_eeheekai4Phue6cahtho")
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -321,7 +321,7 @@ class CandidatureOther(TestCase):
|
|||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_delete_other(self):
|
def test_delete_other(self):
|
||||||
self.assignment.set_candidate(self.user)
|
self.assignment.add_candidate(self.user)
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse("assignment-candidature-other", args=[self.assignment.pk]),
|
reverse("assignment-candidature-other", args=[self.assignment.pk]),
|
||||||
{"user": self.user.pk},
|
{"user": self.user.pk},
|
||||||
@ -343,7 +343,7 @@ class CandidatureOther(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_delete_other_when_finished(self):
|
def test_delete_other_when_finished(self):
|
||||||
self.assignment.set_candidate(self.user)
|
self.assignment.add_candidate(self.user)
|
||||||
self.assignment.set_phase(Assignment.PHASE_FINISHED)
|
self.assignment.set_phase(Assignment.PHASE_FINISHED)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
|
|
||||||
@ -355,7 +355,7 @@ class CandidatureOther(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_delete_other_during_voting(self):
|
def test_delete_other_during_voting(self):
|
||||||
self.assignment.set_candidate(self.user)
|
self.assignment.add_candidate(self.user)
|
||||||
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
|
|
||||||
@ -372,7 +372,7 @@ class CandidatureOther(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_other_during_voting_non_admin(self):
|
def test_delete_other_during_voting_non_admin(self):
|
||||||
self.assignment.set_candidate(self.user)
|
self.assignment.add_candidate(self.user)
|
||||||
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
self.assignment.set_phase(Assignment.PHASE_VOTING)
|
||||||
self.assignment.save()
|
self.assignment.save()
|
||||||
admin = get_user_model().objects.get(username="admin")
|
admin = get_user_model().objects.get(username="admin")
|
||||||
@ -408,7 +408,7 @@ class MarkElectedOtherUser(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_mark_elected(self):
|
def test_mark_elected(self):
|
||||||
self.assignment.set_candidate(
|
self.assignment.add_candidate(
|
||||||
get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev")
|
get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev")
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -437,46 +437,3 @@ class MarkElectedOtherUser(TestCase):
|
|||||||
.elected.filter(username="test_user_Oonei3rahji5jugh1eev")
|
.elected.filter(username="test_user_Oonei3rahji5jugh1eev")
|
||||||
.exists()
|
.exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateAssignmentPoll(TestCase):
|
|
||||||
"""
|
|
||||||
Tests updating polls of assignments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
self.assignment = Assignment.objects.create(
|
|
||||||
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
|
|
||||||
)
|
|
||||||
self.assignment.set_candidate(get_user_model().objects.get(username="admin"))
|
|
||||||
self.poll = self.assignment.create_poll()
|
|
||||||
|
|
||||||
def test_invalid_votesvalid_value(self):
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
|
||||||
{"assignment_id": self.assignment.pk, "votesvalid": "-3"},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_invalid_votesinvalid_value(self):
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
|
||||||
{"assignment_id": self.assignment.pk, "votesinvalid": "-3"},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_invalid_votescast_value(self):
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
|
||||||
{"assignment_id": self.assignment.pk, "votescast": "-3"},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_empty_value_for_votesvalid(self):
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("assignmentpoll-detail", args=[self.poll.pk]),
|
|
||||||
{"assignment_id": self.assignment.pk, "votesvalid": ""},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
@ -9,7 +9,7 @@ from openslides import __license__ as license, __url__ as url, __version__ as ve
|
|||||||
from openslides.core.config import ConfigVariable, config
|
from openslides.core.config import ConfigVariable, config
|
||||||
from openslides.core.models import Projector
|
from openslides.core.models import Projector
|
||||||
from openslides.utils.rest_api import ValidationError
|
from openslides.utils.rest_api import ValidationError
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db(transaction=False)
|
@pytest.mark.django_db(transaction=False)
|
||||||
|
@ -13,8 +13,8 @@ from openslides.core.models import Projector, Tag
|
|||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.auth import get_group_model
|
from openslides.utils.auth import get_group_model
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
||||||
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
|
|
||||||
@ -107,7 +107,6 @@ class Projection(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]),
|
||||||
{"elements": elements},
|
{"elements": elements},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -117,9 +116,7 @@ class Projection(TestCase):
|
|||||||
|
|
||||||
def test_add_element_without_name(self):
|
def test_add_element_without_name(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]), {"elements": [{}]}
|
||||||
{"elements": [{}]},
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -134,7 +131,7 @@ class Projection(TestCase):
|
|||||||
inform_changed_data(admin)
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]), {}, format="json"
|
reverse("projector-project", args=[self.projector.pk]), {}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
@ -142,9 +139,7 @@ class Projection(TestCase):
|
|||||||
self.projector.elements = [{"name": "core/clock"}]
|
self.projector.elements = [{"name": "core/clock"}]
|
||||||
self.projector.save()
|
self.projector.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]), {"elements": []}
|
||||||
{"elements": []},
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -157,7 +152,6 @@ class Projection(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]),
|
||||||
{"append_to_history": element},
|
{"append_to_history": element},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -173,7 +167,6 @@ class Projection(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]),
|
||||||
{"delete_last_history_element": True},
|
{"delete_last_history_element": True},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -186,7 +179,6 @@ class Projection(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("projector-project", args=[self.projector.pk]),
|
reverse("projector-project", args=[self.projector.pk]),
|
||||||
{"preview": elements},
|
{"preview": elements},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.projector = Projector.objects.get(pk=1)
|
self.projector = Projector.objects.get(pk=1)
|
||||||
@ -265,7 +257,6 @@ class ConfigViewSet(TestCase):
|
|||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
reverse("config-detail", args=["agenda_start_event_date_time"]),
|
reverse("config-detail", args=["agenda_start_event_date_time"]),
|
||||||
{"value": None},
|
{"value": None},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(config["agenda_start_event_date_time"], None)
|
self.assertEqual(config["agenda_start_event_date_time"], None)
|
||||||
@ -277,7 +268,6 @@ class ConfigViewSet(TestCase):
|
|||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
reverse("config-detail", args=["motions_identifier_min_digits"]),
|
reverse("config-detail", args=["motions_identifier_min_digits"]),
|
||||||
{"value": None},
|
{"value": None},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -316,9 +306,7 @@ class ConfigViewSet(TestCase):
|
|||||||
self.degrade_admin(can_manage_logos_and_fonts=True)
|
self.degrade_admin(can_manage_logos_and_fonts=True)
|
||||||
value = self.get_static_config_value()
|
value = self.get_static_config_value()
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
reverse("config-detail", args=[self.logo_config_key]),
|
reverse("config-detail", args=[self.logo_config_key]), {"value": value}
|
||||||
{"value": value},
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(config[self.logo_config_key], value)
|
self.assertEqual(config[self.logo_config_key], value)
|
||||||
@ -332,7 +320,6 @@ class ConfigViewSet(TestCase):
|
|||||||
{"key": self.string_config_key, "value": string_value},
|
{"key": self.string_config_key, "value": string_value},
|
||||||
{"key": self.logo_config_key, "value": logo_value},
|
{"key": self.logo_config_key, "value": logo_value},
|
||||||
],
|
],
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["errors"], {})
|
self.assertEqual(response.data["errors"], {})
|
||||||
@ -345,7 +332,6 @@ class ConfigViewSet(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-bulk-update"),
|
reverse("config-bulk-update"),
|
||||||
[{"key": self.string_config_key, "value": string_value}],
|
[{"key": self.string_config_key, "value": string_value}],
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
@ -355,7 +341,6 @@ class ConfigViewSet(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-bulk-update"),
|
reverse("config-bulk-update"),
|
||||||
{"key": self.string_config_key, "value": string_value},
|
{"key": self.string_config_key, "value": string_value},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
@ -363,16 +348,14 @@ class ConfigViewSet(TestCase):
|
|||||||
def test_bulk_update_no_key(self):
|
def test_bulk_update_no_key(self):
|
||||||
string_value = "test_value_glwe32qc&Lml2lclmqmc"
|
string_value = "test_value_glwe32qc&Lml2lclmqmc"
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-bulk-update"), [{"value": string_value}], format="json"
|
reverse("config-bulk-update"), [{"value": string_value}]
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
def test_bulk_update_no_value(self):
|
def test_bulk_update_no_value(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-bulk-update"),
|
reverse("config-bulk-update"), [{"key": self.string_config_key}]
|
||||||
[{"key": self.string_config_key}],
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
@ -384,7 +367,7 @@ class ConfigViewSet(TestCase):
|
|||||||
"motions_preamble"
|
"motions_preamble"
|
||||||
] = "test_preamble_2390jvwohjwo1oigefoq" # Group motions
|
] = "test_preamble_2390jvwohjwo1oigefoq" # Group motions
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-reset-groups"), ["General", "Agenda"], format="json"
|
reverse("config-reset-groups"), ["General", "Agenda"]
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(config["general_event_name"], "OpenSlides")
|
self.assertEqual(config["general_event_name"], "OpenSlides")
|
||||||
@ -394,15 +377,11 @@ class ConfigViewSet(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_reset_group_wrong_format_1(self):
|
def test_reset_group_wrong_format_1(self):
|
||||||
response = self.client.post(
|
response = self.client.post(reverse("config-reset-groups"), {"wrong": "format"})
|
||||||
reverse("config-reset-groups"), {"wrong": "format"}, format="json"
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def test_reset_group_wrong_format_2(self):
|
def test_reset_group_wrong_format_2(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("config-reset-groups"),
|
reverse("config-reset-groups"), ["some_string", {"wrong": "format"}]
|
||||||
["some_string", {"wrong": "format"}],
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ class TestCreation(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("mediafile-list"),
|
reverse("mediafile-list"),
|
||||||
{"title": "test_title_ahyo1uifoo9Aiph2av5a", "mediafile": self.file},
|
{"title": "test_title_ahyo1uifoo9Aiph2av5a", "mediafile": self.file},
|
||||||
|
format="multipart",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
mediafile = Mediafile.objects.get()
|
mediafile = Mediafile.objects.get()
|
||||||
@ -170,8 +171,8 @@ class TestCreation(TestCase):
|
|||||||
reverse("mediafile-list"),
|
reverse("mediafile-list"),
|
||||||
{
|
{
|
||||||
"title": "test_title_dggjwevBnUngelkdviom",
|
"title": "test_title_dggjwevBnUngelkdviom",
|
||||||
"mediafile": self.file,
|
"is_directory": True,
|
||||||
"access_groups_id": json.dumps([2, 4]),
|
"access_groups_id": [2, 4],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
@ -268,7 +269,6 @@ class TestUpdate(TestCase):
|
|||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
reverse("mediafile-detail", args=[self.mediafileA.pk]),
|
reverse("mediafile-detail", args=[self.mediafileA.pk]),
|
||||||
{"title": self.mediafileA.title, "parent_id": None},
|
{"title": self.mediafileA.title, "parent_id": None},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
mediafile = Mediafile.objects.get(pk=self.mediafileA.pk)
|
mediafile = Mediafile.objects.get(pk=self.mediafileA.pk)
|
||||||
|
980
tests/integration/motions/test_motions.py
Normal file
980
tests/integration/motions/test_motions.py
Normal file
@ -0,0 +1,980 @@
|
|||||||
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from openslides.core.config import config
|
||||||
|
from openslides.core.models import Tag
|
||||||
|
from openslides.motions.models import (
|
||||||
|
Category,
|
||||||
|
Motion,
|
||||||
|
MotionChangeRecommendation,
|
||||||
|
MotionComment,
|
||||||
|
MotionCommentSection,
|
||||||
|
MotionPoll,
|
||||||
|
MotionVote,
|
||||||
|
Submitter,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
from openslides.poll.models import BasePoll
|
||||||
|
from openslides.utils.auth import get_group_model
|
||||||
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
|
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DEFAULT_PK, GROUP_DELEGATE_PK
|
||||||
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
from ..helpers import count_queries
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=False)
|
||||||
|
def test_motion_db_queries():
|
||||||
|
"""
|
||||||
|
Tests that only the following db queries are done:
|
||||||
|
* 1 requests to get the list of all motions,
|
||||||
|
* 1 request to get the associated workflow
|
||||||
|
* 1 request for all motion comments
|
||||||
|
* 1 request for all motion comment sections required for the comments
|
||||||
|
* 1 request for all users required for the read_groups of the sections
|
||||||
|
* 1 request to get the agenda item,
|
||||||
|
* 1 request to get the list of speakers,
|
||||||
|
* 1 request to get the polls,
|
||||||
|
* 1 request to get all poll groups,
|
||||||
|
* 1 request to get all poll voted users,
|
||||||
|
* 1 request to get all options for all polls,
|
||||||
|
* 1 request to get all votes for all options,
|
||||||
|
* 1 request to get all users for all votes,
|
||||||
|
* 1 request to get the attachments,
|
||||||
|
* 1 request to get the tags,
|
||||||
|
* 2 requests to get the submitters and supporters,
|
||||||
|
* 1 request for change_recommendations.
|
||||||
|
|
||||||
|
Two comment sections are created and for each motions two comments.
|
||||||
|
"""
|
||||||
|
section1 = MotionCommentSection.objects.create(name="test_section")
|
||||||
|
section2 = MotionCommentSection.objects.create(name="test_section")
|
||||||
|
|
||||||
|
user1 = get_user_model().objects.create_user(
|
||||||
|
username="test_username_Iena7vahyaiphaangeaV",
|
||||||
|
password="test_password_oomie4jahNgook1ooDee",
|
||||||
|
)
|
||||||
|
user2 = get_user_model().objects.create_user(
|
||||||
|
username="test_username_ohj4eiN3ejali9ahng6e",
|
||||||
|
password="test_password_Coo3ong1cheeveiD3sho",
|
||||||
|
)
|
||||||
|
user3 = get_user_model().objects.create_user(
|
||||||
|
username="test_username_oe2Yei9Tho8see1Reija",
|
||||||
|
password="test_password_faij5aeBingaec5Jeila",
|
||||||
|
)
|
||||||
|
|
||||||
|
for index in range(10):
|
||||||
|
motion = Motion.objects.create(title=f"motion{index}")
|
||||||
|
|
||||||
|
MotionComment.objects.create(
|
||||||
|
comment="test_comment", motion=motion, section=section1
|
||||||
|
)
|
||||||
|
MotionComment.objects.create(
|
||||||
|
comment="test_comment2", motion=motion, section=section2
|
||||||
|
)
|
||||||
|
|
||||||
|
get_user_model().objects.create_user(
|
||||||
|
username=f"user_{index}", password="password"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create some fully populated polls:
|
||||||
|
poll1 = MotionPoll.objects.create(
|
||||||
|
motion=motion,
|
||||||
|
title="test_title_XeejaeFez3chahpei9qu",
|
||||||
|
pollmethod="YNA",
|
||||||
|
type=BasePoll.TYPE_NAMED,
|
||||||
|
)
|
||||||
|
poll1.create_options()
|
||||||
|
option = poll1.options.get()
|
||||||
|
MotionVote.objects.create(
|
||||||
|
user=user1, option=option, value="Y", weight=Decimal(1)
|
||||||
|
)
|
||||||
|
poll1.voted.add(user1)
|
||||||
|
MotionVote.objects.create(
|
||||||
|
user=user2, option=option, value="N", weight=Decimal(1)
|
||||||
|
)
|
||||||
|
poll1.voted.add(user2)
|
||||||
|
|
||||||
|
poll2 = MotionPoll.objects.create(
|
||||||
|
motion=motion,
|
||||||
|
title="test_title_iecuW7eekeGh4uunow1e",
|
||||||
|
pollmethod="YNA",
|
||||||
|
type=BasePoll.TYPE_NAMED,
|
||||||
|
)
|
||||||
|
poll2.create_options()
|
||||||
|
option = poll2.options.get()
|
||||||
|
MotionVote.objects.create(
|
||||||
|
user=user2, option=option, value="Y", weight=Decimal(1)
|
||||||
|
)
|
||||||
|
poll2.voted.add(user2)
|
||||||
|
MotionVote.objects.create(
|
||||||
|
user=user3, option=option, value="N", weight=Decimal(1)
|
||||||
|
)
|
||||||
|
poll2.voted.add(user3)
|
||||||
|
|
||||||
|
assert count_queries(Motion.get_elements) == 18
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMotion(TestCase):
|
||||||
|
"""
|
||||||
|
Tests motion creation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
"""
|
||||||
|
Tests that a motion is created with a specific title and text.
|
||||||
|
|
||||||
|
The created motion should have an identifier and the admin user should
|
||||||
|
be the submitter.
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_OoCoo3MeiT9li5Iengu9",
|
||||||
|
"text": "test_text_thuoz0iecheiheereiCi",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.title, "test_title_OoCoo3MeiT9li5Iengu9")
|
||||||
|
self.assertEqual(motion.identifier, "1")
|
||||||
|
self.assertTrue(motion.submitters.exists())
|
||||||
|
self.assertEqual(motion.submitters.get().user.username, "admin")
|
||||||
|
|
||||||
|
def test_with_reason(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_saib4hiHaifo9ohp9yie",
|
||||||
|
"text": "test_text_shahhie8Ej4mohvoorie",
|
||||||
|
"reason": "test_reason_Ou8GivahYivoh3phoh9c",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(
|
||||||
|
Motion.objects.get().reason, "test_reason_Ou8GivahYivoh3phoh9c"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_without_data(self):
|
||||||
|
response = self.client.post(reverse("motion-list"), {})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertTrue("title" in response.data)
|
||||||
|
|
||||||
|
def test_without_text(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"), {"title": "test_title_dlofp23m9O(ZD2d1lwHG"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
str(response.data["detail"][0]), "The text field may not be blank."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_category(self):
|
||||||
|
category = Category.objects.create(
|
||||||
|
name="test_category_name_CiengahzooH4ohxietha",
|
||||||
|
prefix="TEST_PREFIX_la0eadaewuec3seoxeiN",
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Air0bahchaiph1ietoo2",
|
||||||
|
"text": "test_text_chaeF9wosh8OowazaiVu",
|
||||||
|
"category_id": category.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.category, category)
|
||||||
|
self.assertEqual(motion.identifier, "TEST_PREFIX_la0eadaewuec3seoxeiN1")
|
||||||
|
|
||||||
|
def test_with_submitters(self):
|
||||||
|
submitter_1 = get_user_model().objects.create_user(
|
||||||
|
username="test_username_ooFe6aebei9ieQui2poo",
|
||||||
|
password="test_password_vie9saiQu5Aengoo9ku0",
|
||||||
|
)
|
||||||
|
submitter_2 = get_user_model().objects.create_user(
|
||||||
|
username="test_username_eeciengoc4aihie5eeSh",
|
||||||
|
password="test_password_peik2Eihu5oTh7siequi",
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_pha7moPh7quoth4paina",
|
||||||
|
"text": "test_text_YooGhae6tiangung5Rie",
|
||||||
|
"submitters_id": [submitter_1.pk, submitter_2.pk],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.submitters.count(), 2)
|
||||||
|
|
||||||
|
def test_with_one_supporter(self):
|
||||||
|
supporter = get_user_model().objects.create_user(
|
||||||
|
username="test_username_ahGhi4Quohyee7ohngie",
|
||||||
|
password="test_password_Nei6aeh8OhY8Aegh1ohX",
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Oecee4Da2Mu9EY6Ui4mu",
|
||||||
|
"text": "test_text_FbhgnTFgkbjdmvcjbffg",
|
||||||
|
"supporters_id": [supporter.pk],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(
|
||||||
|
motion.supporters.get().username, "test_username_ahGhi4Quohyee7ohngie"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_tag(self):
|
||||||
|
tag = Tag.objects.create(name="test_tag_iRee3kiecoos4rorohth")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_Hahke4loos4eiduNiid9",
|
||||||
|
"text": "test_text_johcho0Ucaibiehieghe",
|
||||||
|
"tags_id": [tag.pk],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.tags.get().name, "test_tag_iRee3kiecoos4rorohth")
|
||||||
|
|
||||||
|
def test_with_workflow(self):
|
||||||
|
"""
|
||||||
|
Test to create a motion with a specific workflow.
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_eemuR5hoo4ru2ahgh5EJ",
|
||||||
|
"text": "test_text_ohviePopahPhoili7yee",
|
||||||
|
"workflow_id": "2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Motion.objects.get().state.workflow_id, 2)
|
||||||
|
|
||||||
|
def test_non_admin(self):
|
||||||
|
"""
|
||||||
|
Test to create a motion by a delegate, non staff user.
|
||||||
|
"""
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
self.admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
self.admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
inform_changed_data(self.admin)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_peiJozae0luew9EeL8bo",
|
||||||
|
"text": "test_text_eHohS8ohr5ahshoah8Oh",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_amendment_motion(self):
|
||||||
|
"""
|
||||||
|
Test to create a motion with a parent motion as staff user.
|
||||||
|
"""
|
||||||
|
parent_motion = self.create_parent_motion()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_doe93Jsjd2sW20dkSl20",
|
||||||
|
"text": "test_text_feS20SksD8D25skmwD25",
|
||||||
|
"parent_id": parent_motion.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
created_motion = Motion.objects.get(pk=int(response.data["id"]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(created_motion.parent, parent_motion)
|
||||||
|
|
||||||
|
def test_amendment_motion_parent_not_exist(self):
|
||||||
|
"""
|
||||||
|
Test to create an amendment motion with a non existing parent.
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_gEjdkW93Wj23KS2s8dSe",
|
||||||
|
"text": "test_text_lfwLIC&AjfsaoijOEusa",
|
||||||
|
"parent_id": 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data, {"detail": "The parent motion does not exist."})
|
||||||
|
|
||||||
|
def test_amendment_motion_non_admin(self):
|
||||||
|
"""
|
||||||
|
Test to create an amendment motion by a delegate. The parents
|
||||||
|
category should be also set on the new motion.
|
||||||
|
"""
|
||||||
|
parent_motion = self.create_parent_motion()
|
||||||
|
category = Category.objects.create(
|
||||||
|
name="test_category_name_Dslk3Fj8s8Ps36S3Kskw",
|
||||||
|
prefix="TEST_PREFIX_L23skfmlq3kslamslS39",
|
||||||
|
)
|
||||||
|
parent_motion.category = category
|
||||||
|
parent_motion.save()
|
||||||
|
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
self.admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
self.admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
inform_changed_data(self.admin)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_fk3a0slalms47KSewnWG",
|
||||||
|
"text": "test_text_al3FMwSCNM31WOmw9ezx",
|
||||||
|
"parent_id": parent_motion.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
created_motion = Motion.objects.get(pk=int(response.data["id"]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(created_motion.parent, parent_motion)
|
||||||
|
self.assertEqual(created_motion.category, category)
|
||||||
|
|
||||||
|
def create_parent_motion(self):
|
||||||
|
"""
|
||||||
|
Returns a new created motion used for testing amendments.
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_3leoeo2qac7830c92j9s",
|
||||||
|
"text": "test_text_9dm3ks9gDuW20Al38L9w",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return Motion.objects.get(pk=int(response.data["id"]))
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveMotion(TestCase):
|
||||||
|
"""
|
||||||
|
Tests retrieving a motion
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_uj5eeSiedohSh3ohyaaj",
|
||||||
|
text="test_text_ithohchaeThohmae5aug",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
for index in range(10):
|
||||||
|
get_user_model().objects.create_user(
|
||||||
|
username=f"user_{index}", password="password"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_guest_state_with_restriction(self):
|
||||||
|
config["general_system_enable_anonymous"] = True
|
||||||
|
guest_client = APIClient()
|
||||||
|
state = self.motion.state
|
||||||
|
state.restriction = ["motions.can_manage"]
|
||||||
|
state.save()
|
||||||
|
# The cache has to be cleared, see:
|
||||||
|
# https://github.com/OpenSlides/OpenSlides/issues/3396
|
||||||
|
inform_changed_data(self.motion)
|
||||||
|
|
||||||
|
response = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_admin_state_with_restriction(self):
|
||||||
|
state = self.motion.state
|
||||||
|
state.restriction = ["motions.can_manage"]
|
||||||
|
state.save()
|
||||||
|
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_submitter_state_with_restriction(self):
|
||||||
|
state = self.motion.state
|
||||||
|
state.restriction = ["is_submitter"]
|
||||||
|
state.save()
|
||||||
|
user = get_user_model().objects.create_user(
|
||||||
|
username="username_ohS2opheikaSa5theijo",
|
||||||
|
password="password_kau4eequaisheeBateef",
|
||||||
|
)
|
||||||
|
Submitter.objects.add(user, self.motion)
|
||||||
|
submitter_client = APIClient()
|
||||||
|
submitter_client.force_login(user)
|
||||||
|
response = submitter_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
admin = get_user_model().objects.get(username="admin")
|
||||||
|
Submitter.objects.add(admin, self.motion)
|
||||||
|
group = get_group_model().objects.get(
|
||||||
|
pk=GROUP_DEFAULT_PK
|
||||||
|
) # Group with pk 1 is for anonymous and default users.
|
||||||
|
permission_string = "users.can_see_name"
|
||||||
|
app_label, codename = permission_string.split(".")
|
||||||
|
permission = group.permissions.get(
|
||||||
|
content_type__app_label=app_label, codename=codename
|
||||||
|
)
|
||||||
|
group.permissions.remove(permission)
|
||||||
|
config["general_system_enable_anonymous"] = True
|
||||||
|
guest_client = APIClient()
|
||||||
|
inform_changed_data(group)
|
||||||
|
inform_changed_data(self.motion)
|
||||||
|
|
||||||
|
response_1 = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response_1.status_code, status.HTTP_200_OK)
|
||||||
|
submitter_id = response_1.data["submitters"][0]["user_id"]
|
||||||
|
response_2 = guest_client.get(reverse("user-detail", args=[submitter_id]))
|
||||||
|
self.assertEqual(response_2.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
extra_user = get_user_model().objects.create_user(
|
||||||
|
username="username_wequePhieFoom0hai3wa",
|
||||||
|
password="password_ooth7taechai5Oocieya",
|
||||||
|
)
|
||||||
|
|
||||||
|
response_3 = guest_client.get(reverse("user-detail", args=[extra_user.pk]))
|
||||||
|
self.assertEqual(response_3.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMotion(TestCase):
|
||||||
|
"""
|
||||||
|
Tests updating motions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_aeng7ahChie3waiR8xoh",
|
||||||
|
text="test_text_xeigheeha7thopubeu4U",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
def test_simple_patch(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
{"identifier": "test_identifier_jieseghohj7OoSah1Ko9"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertAutoupdate(motion)
|
||||||
|
self.assertAutoupdate(motion.agenda_item)
|
||||||
|
self.assertAutoupdate(motion.list_of_speakers)
|
||||||
|
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
|
||||||
|
self.assertEqual(motion.identifier, "test_identifier_jieseghohj7OoSah1Ko9")
|
||||||
|
|
||||||
|
def test_patch_as_anonymous_without_manage_perms(self):
|
||||||
|
config["general_system_enable_anonymous"] = True
|
||||||
|
guest_client = APIClient()
|
||||||
|
response = guest_client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
{"identifier": "test_identifier_4g2jgj1wrnmvvIRhtqqPO84WD"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.identifier, "1")
|
||||||
|
|
||||||
|
def test_patch_empty_text(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]), {"text": ""}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.text, "test_text_xeigheeha7thopubeu4U")
|
||||||
|
|
||||||
|
def test_patch_amendment_paragraphs_no_manage_perms(self):
|
||||||
|
admin = get_user_model().objects.get(username="admin")
|
||||||
|
admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
Submitter.objects.add(admin, self.motion)
|
||||||
|
self.motion.state.allow_submitter_edit = True
|
||||||
|
self.motion.state.save()
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
{"amendment_paragraphs": ["test_paragraph_39fo8qcpcaFMmjfaD2Lb"]},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertTrue(isinstance(motion.amendment_paragraphs, list))
|
||||||
|
self.assertEqual(len(motion.amendment_paragraphs), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
motion.amendment_paragraphs[0], "test_paragraph_39fo8qcpcaFMmjfaD2Lb"
|
||||||
|
)
|
||||||
|
self.assertEqual(motion.text, "")
|
||||||
|
|
||||||
|
def test_patch_workflow(self):
|
||||||
|
"""
|
||||||
|
Tests to only update the workflow of a motion.
|
||||||
|
"""
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]), {"workflow_id": "2"}
|
||||||
|
)
|
||||||
|
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
|
||||||
|
self.assertEqual(motion.workflow_id, 2)
|
||||||
|
|
||||||
|
def test_patch_category(self):
|
||||||
|
"""
|
||||||
|
Tests to only update the category of a motion. Expects the
|
||||||
|
category_weight to be resetted.
|
||||||
|
"""
|
||||||
|
category = Category.objects.create(
|
||||||
|
name="test_category_name_FE3jO(Fm83doqqlwcvlv",
|
||||||
|
prefix="test_prefix_w3ofg2mv79UGFqjk3f8h",
|
||||||
|
)
|
||||||
|
self.motion.category_weight = 1
|
||||||
|
self.motion.save()
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
{"category_id": category.pk},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.category, category)
|
||||||
|
self.assertEqual(motion.category_weight, 10000)
|
||||||
|
|
||||||
|
def test_patch_supporters(self):
|
||||||
|
supporter = get_user_model().objects.create_user(
|
||||||
|
username="test_username_ieB9eicah0uqu6Phoovo",
|
||||||
|
password="test_password_XaeTe3aesh8ohg6Cohwo",
|
||||||
|
)
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
json.dumps({"supporters_id": [supporter.pk]}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
|
||||||
|
self.assertEqual(
|
||||||
|
motion.supporters.get().username, "test_username_ieB9eicah0uqu6Phoovo"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_patch_supporters_non_manager(self):
|
||||||
|
non_admin = get_user_model().objects.create_user(
|
||||||
|
username="test_username_uqu6PhoovieB9eicah0o",
|
||||||
|
password="test_password_Xaesh8ohg6CoheTe3awo",
|
||||||
|
)
|
||||||
|
self.client.login(
|
||||||
|
username="test_username_uqu6PhoovieB9eicah0o",
|
||||||
|
password="test_password_Xaesh8ohg6CoheTe3awo",
|
||||||
|
)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
Submitter.objects.add(non_admin, self.motion)
|
||||||
|
motion.supporters.clear()
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
json.dumps({"supporters_id": [1]}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
# Forbidden because of changed workflow state.
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_removal_of_supporters(self):
|
||||||
|
# No cache used here.
|
||||||
|
admin = get_user_model().objects.get(username="admin")
|
||||||
|
group_admin = admin.groups.get(name="Admin")
|
||||||
|
admin.groups.remove(group_admin)
|
||||||
|
Submitter.objects.add(admin, self.motion)
|
||||||
|
supporter = get_user_model().objects.create_user(
|
||||||
|
username="test_username_ahshi4oZin0OoSh9chee",
|
||||||
|
password="test_password_Sia8ahgeenixu5cei2Ib",
|
||||||
|
)
|
||||||
|
self.motion.supporters.add(supporter)
|
||||||
|
config["motions_remove_supporters"] = True
|
||||||
|
self.assertEqual(self.motion.supporters.count(), 1)
|
||||||
|
inform_changed_data((admin, self.motion))
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motion-detail", args=[self.motion.pk]),
|
||||||
|
{"title": "new_title_ohph1aedie5Du8sai2ye"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Forbidden because of changed workflow state.
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMotion(TestCase):
|
||||||
|
"""
|
||||||
|
Tests deleting motions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_acle3fa93l11lwlkcc31",
|
||||||
|
text="test_text_f390sjfyycj29ss56sro",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
def test_simple_delete(self):
|
||||||
|
response = self.client.delete(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
motions = Motion.objects.count()
|
||||||
|
self.assertEqual(motions, 0)
|
||||||
|
|
||||||
|
def make_admin_delegate(self):
|
||||||
|
self.admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
self.admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
inform_changed_data(self.admin)
|
||||||
|
|
||||||
|
def put_motion_in_complex_workflow(self):
|
||||||
|
workflow = Workflow.objects.get(name="Complex Workflow")
|
||||||
|
self.motion.reset_state(workflow=workflow)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
def test_delete_foreign_motion_as_delegate(self):
|
||||||
|
self.make_admin_delegate()
|
||||||
|
self.put_motion_in_complex_workflow()
|
||||||
|
|
||||||
|
response = self.client.delete(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_delete_own_motion_as_delegate(self):
|
||||||
|
self.make_admin_delegate()
|
||||||
|
self.put_motion_in_complex_workflow()
|
||||||
|
Submitter.objects.add(self.admin, self.motion)
|
||||||
|
|
||||||
|
response = self.client.delete(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
motions = Motion.objects.count()
|
||||||
|
self.assertEqual(motions, 0)
|
||||||
|
|
||||||
|
def test_delete_with_two_change_recommendations(self):
|
||||||
|
self.cr1 = MotionChangeRecommendation.objects.create(
|
||||||
|
motion=self.motion, internal=False, line_from=1, line_to=1
|
||||||
|
)
|
||||||
|
self.cr2 = MotionChangeRecommendation.objects.create(
|
||||||
|
motion=self.motion, internal=False, line_from=2, line_to=2
|
||||||
|
)
|
||||||
|
response = self.client.delete(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
motions = Motion.objects.count()
|
||||||
|
self.assertEqual(motions, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class ManageMultipleSubmitters(TestCase):
|
||||||
|
"""
|
||||||
|
Tests adding and removing of submitters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
|
||||||
|
self.admin = get_user_model().objects.get()
|
||||||
|
self.motion1 = Motion(
|
||||||
|
title="test_title_SlqfMw(waso0saWMPqcZ",
|
||||||
|
text="test_text_f30skclqS9wWF=xdfaSL",
|
||||||
|
)
|
||||||
|
self.motion1.save()
|
||||||
|
self.motion2 = Motion(
|
||||||
|
title="test_title_f>FLEim38MC2m9PFp2jG",
|
||||||
|
text="test_text_kg39KFGm,ao)22FK9lLu",
|
||||||
|
)
|
||||||
|
self.motion2.save()
|
||||||
|
|
||||||
|
def test_set_submitters(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-manage-multiple-submitters"),
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"motions": [
|
||||||
|
{"id": self.motion1.id, "submitters": [self.admin.pk]},
|
||||||
|
{"id": self.motion2.id, "submitters": [self.admin.pk]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(self.motion1.submitters.count(), 1)
|
||||||
|
self.assertEqual(self.motion2.submitters.count(), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
self.motion1.submitters.get().user.pk, self.motion2.submitters.get().user.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_non_existing_user(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-manage-multiple-submitters"),
|
||||||
|
{"motions": [{"id": self.motion1.id, "submitters": [1337]}]},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(self.motion1.submitters.count(), 0)
|
||||||
|
|
||||||
|
def test_add_user_no_data(self):
|
||||||
|
response = self.client.post(reverse("motion-manage-multiple-submitters"))
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(self.motion1.submitters.count(), 0)
|
||||||
|
self.assertEqual(self.motion2.submitters.count(), 0)
|
||||||
|
|
||||||
|
def test_add_user_invalid_data(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-manage-multiple-submitters"), {"motions": ["invalid_str"]}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(self.motion1.submitters.count(), 0)
|
||||||
|
self.assertEqual(self.motion2.submitters.count(), 0)
|
||||||
|
|
||||||
|
def test_add_without_permission(self):
|
||||||
|
admin = get_user_model().objects.get(username="admin")
|
||||||
|
admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-manage-multiple-submitters"),
|
||||||
|
{"motions": [{"id": self.motion1.id, "submitters": [self.admin.pk]}]},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(self.motion1.submitters.count(), 0)
|
||||||
|
self.assertEqual(self.motion2.submitters.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class SupportMotion(TestCase):
|
||||||
|
"""
|
||||||
|
Tests supporting a motion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
self.admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
inform_changed_data(self.admin)
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_chee7ahCha6bingaew4e",
|
||||||
|
text="test_text_birah1theL9ooseeFaip",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
def test_support(self):
|
||||||
|
config["motions_min_supporters"] = 1
|
||||||
|
|
||||||
|
response = self.client.post(reverse("motion-support", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data, {"detail": "You have supported this motion successfully."}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unsupport(self):
|
||||||
|
config["motions_min_supporters"] = 1
|
||||||
|
self.motion.supporters.add(self.admin)
|
||||||
|
response = self.client.delete(reverse("motion-support", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data, {"detail": "You have unsupported this motion successfully."}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetState(TestCase):
|
||||||
|
"""
|
||||||
|
Tests setting a state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_iac4ohquie9Ku6othieC",
|
||||||
|
text="test_text_Xohphei6Oobee0Evooyu",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
|
||||||
|
|
||||||
|
def test_set_state(self):
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-state", args=[self.motion.pk]),
|
||||||
|
{"state": self.state_id_accepted},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data, {"detail": "The state of the motion was set to accepted."}
|
||||||
|
)
|
||||||
|
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, "accepted")
|
||||||
|
|
||||||
|
def test_set_state_with_string(self):
|
||||||
|
# Using a string is not allowed even if it is the correct name of the state.
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-state", args=[self.motion.pk]), {"state": "accepted"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data, {"detail": "Invalid data. State must be an integer."}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_unknown_state(self):
|
||||||
|
invalid_state_id = 0
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-state", args=[self.motion.pk]),
|
||||||
|
{"state": invalid_state_id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "You can not set the state to {0}.",
|
||||||
|
"args": [str(invalid_state_id)],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reset(self):
|
||||||
|
self.motion.set_state(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
response = self.client.put(reverse("motion-set-state", args=[self.motion.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data, {"detail": "The state of the motion was set to submitted."}
|
||||||
|
)
|
||||||
|
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, "submitted")
|
||||||
|
|
||||||
|
|
||||||
|
class SetRecommendation(TestCase):
|
||||||
|
"""
|
||||||
|
Tests setting a recommendation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.motion = Motion(
|
||||||
|
title="test_title_ahfooT5leilahcohJ2uz",
|
||||||
|
text="test_text_enoogh7OhPoo6eohoCus",
|
||||||
|
)
|
||||||
|
self.motion.save()
|
||||||
|
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
|
||||||
|
|
||||||
|
def test_set_recommendation(self):
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": self.state_id_accepted},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "The recommendation of the motion was set to {0}.",
|
||||||
|
"args": ["Acceptance"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_state_with_string(self):
|
||||||
|
# Using a string is not allowed even if it is the correct name of the state.
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": "accepted"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{"detail": "Invalid data. Recommendation must be an integer."},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_unknown_recommendation(self):
|
||||||
|
invalid_state_id = 0
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": invalid_state_id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "You can not set the recommendation to {0}.",
|
||||||
|
"args": [str(invalid_state_id)],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_invalid_recommendation(self):
|
||||||
|
# This is a valid state id, but this state is not recommendable because it belongs to a different workflow.
|
||||||
|
invalid_state_id = 6 # State 'permitted'
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": invalid_state_id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "You can not set the recommendation to {0}.",
|
||||||
|
"args": [str(invalid_state_id)],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_invalid_recommendation_2(self):
|
||||||
|
# This is a valid state id, but this state is not recommendable because it has not recommendation label
|
||||||
|
invalid_state_id = 1 # State 'submitted'
|
||||||
|
self.motion.set_state(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": invalid_state_id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "You can not set the recommendation to {0}.",
|
||||||
|
"args": [str(invalid_state_id)],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reset(self):
|
||||||
|
self.motion.set_recommendation(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "The recommendation of the motion was set to {0}.",
|
||||||
|
"args": ["None"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertTrue(Motion.objects.get(pk=self.motion.pk).recommendation is None)
|
||||||
|
|
||||||
|
def test_set_recommendation_to_current_state(self):
|
||||||
|
self.motion.set_state(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("motion-set-recommendation", args=[self.motion.pk]),
|
||||||
|
{"recommendation": self.state_id_accepted},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"detail": "The recommendation of the motion was set to {0}.",
|
||||||
|
"args": ["Acceptance"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted"
|
||||||
|
)
|
1107
tests/integration/motions/test_polls.py
Normal file
1107
tests/integration/motions/test_polls.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ from django.test.client import Client
|
|||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.motions.models import Motion
|
from openslides.motions.models import Motion
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
class AnonymousRequests(TestCase):
|
class AnonymousRequests(TestCase):
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
|
|
||||||
|
@ -3,12 +3,15 @@ import json
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWhoAmIView(TestCase):
|
class TestWhoAmIView(TestCase):
|
||||||
url = reverse("user_whoami")
|
url = reverse("user_whoami")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
@ -44,6 +47,9 @@ class TestWhoAmIView(TestCase):
|
|||||||
class TestUserLogoutView(TestCase):
|
class TestUserLogoutView(TestCase):
|
||||||
url = reverse("user_logout")
|
url = reverse("user_logout")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from rest_framework.test import APIClient
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.users.models import Group, PersonalNote, User
|
from openslides.users.models import Group, PersonalNote, User
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ...common_groups import (
|
from ...common_groups import (
|
||||||
GROUP_ADMIN_PK,
|
GROUP_ADMIN_PK,
|
||||||
@ -196,7 +196,6 @@ class UserUpdate(TestCase):
|
|||||||
response = admin_client.patch(
|
response = admin_client.patch(
|
||||||
reverse("user-detail", args=[user_pk]),
|
reverse("user-detail", args=[user_pk]),
|
||||||
{"username": "admin", "is_active": False},
|
{"username": "admin", "is_active": False},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
@ -268,7 +267,7 @@ class UserDelete(TestCase):
|
|||||||
ids.append(user.id)
|
ids.append(user.id)
|
||||||
|
|
||||||
response = self.admin_client.post(
|
response = self.admin_client.post(
|
||||||
reverse("user-bulk-delete"), {"user_ids": ids}, format="json"
|
reverse("user-bulk-delete"), {"user_ids": ids}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(User.objects.filter(pk__in=ids).exists())
|
self.assertFalse(User.objects.filter(pk__in=ids).exists())
|
||||||
@ -276,7 +275,7 @@ class UserDelete(TestCase):
|
|||||||
def test_bulk_delete_self(self):
|
def test_bulk_delete_self(self):
|
||||||
""" The own id should be excluded, so nothing should happen. """
|
""" The own id should be excluded, so nothing should happen. """
|
||||||
response = self.admin_client.post(
|
response = self.admin_client.post(
|
||||||
reverse("user-bulk-delete"), {"user_ids": [1]}, format="json"
|
reverse("user-bulk-delete"), {"user_ids": [1]}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertTrue(User.objects.filter(pk=1).exists())
|
self.assertTrue(User.objects.filter(pk=1).exists())
|
||||||
@ -416,9 +415,7 @@ class UserPassword(TestCase):
|
|||||||
self.assertTrue(user2.check_password(default_password2))
|
self.assertTrue(user2.check_password(default_password2))
|
||||||
|
|
||||||
response = self.admin_client.post(
|
response = self.admin_client.post(
|
||||||
reverse("user-bulk-generate-passwords"),
|
reverse("user-bulk-generate-passwords"), {"user_ids": [user1.id, user2.id]}
|
||||||
{"user_ids": [user1.id, user2.id]},
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -450,7 +447,6 @@ class UserPassword(TestCase):
|
|||||||
response = self.admin_client.post(
|
response = self.admin_client.post(
|
||||||
reverse("user-bulk-reset-passwords-to-default"),
|
reverse("user-bulk-reset-passwords-to-default"),
|
||||||
{"user_ids": [user1.id, user2.id]},
|
{"user_ids": [user1.id, user2.id]},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -478,7 +474,6 @@ class UserBulkSetState(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("user-bulk-set-state"),
|
reverse("user-bulk-set-state"),
|
||||||
{"user_ids": [1], "field": "is_present", "value": False},
|
{"user_ids": [1], "field": "is_present", "value": False},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertTrue(User.objects.get().is_active)
|
self.assertTrue(User.objects.get().is_active)
|
||||||
@ -489,7 +484,6 @@ class UserBulkSetState(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("user-bulk-set-state"),
|
reverse("user-bulk-set-state"),
|
||||||
{"user_ids": [1], "field": "invalid", "value": False},
|
{"user_ids": [1], "field": "invalid", "value": False},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertTrue(User.objects.get().is_active)
|
self.assertTrue(User.objects.get().is_active)
|
||||||
@ -500,7 +494,6 @@ class UserBulkSetState(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("user-bulk-set-state"),
|
reverse("user-bulk-set-state"),
|
||||||
{"user_ids": [1], "field": "is_active", "value": "invalid"},
|
{"user_ids": [1], "field": "is_active", "value": "invalid"},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertTrue(User.objects.get().is_active)
|
self.assertTrue(User.objects.get().is_active)
|
||||||
@ -511,7 +504,6 @@ class UserBulkSetState(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("user-bulk-set-state"),
|
reverse("user-bulk-set-state"),
|
||||||
{"user_ids": [1], "field": "is_active", "value": False},
|
{"user_ids": [1], "field": "is_active", "value": False},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertTrue(User.objects.get().is_active)
|
self.assertTrue(User.objects.get().is_active)
|
||||||
@ -539,7 +531,6 @@ class UserBulkAlterGroups(TestCase):
|
|||||||
"action": "add",
|
"action": "add",
|
||||||
"group_ids": [GROUP_DELEGATE_PK, GROUP_STAFF_PK],
|
"group_ids": [GROUP_DELEGATE_PK, GROUP_STAFF_PK],
|
||||||
},
|
},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(self.user.groups.count(), 2)
|
self.assertEqual(self.user.groups.count(), 2)
|
||||||
@ -558,7 +549,6 @@ class UserBulkAlterGroups(TestCase):
|
|||||||
"action": "remove",
|
"action": "remove",
|
||||||
"group_ids": [GROUP_DEFAULT_PK, GROUP_STAFF_PK],
|
"group_ids": [GROUP_DEFAULT_PK, GROUP_STAFF_PK],
|
||||||
},
|
},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(self.user.groups.count(), 1)
|
self.assertEqual(self.user.groups.count(), 1)
|
||||||
@ -574,7 +564,6 @@ class UserBulkAlterGroups(TestCase):
|
|||||||
"action": "add",
|
"action": "add",
|
||||||
"group_ids": [GROUP_DELEGATE_PK],
|
"group_ids": [GROUP_DELEGATE_PK],
|
||||||
},
|
},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(self.admin.groups.count(), 1)
|
self.assertEqual(self.admin.groups.count(), 1)
|
||||||
@ -588,7 +577,6 @@ class UserBulkAlterGroups(TestCase):
|
|||||||
"action": "invalid",
|
"action": "invalid",
|
||||||
"group_ids": [GROUP_DELEGATE_PK],
|
"group_ids": [GROUP_DELEGATE_PK],
|
||||||
},
|
},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -614,7 +602,7 @@ class UserMassImport(TestCase):
|
|||||||
"groups_id": [],
|
"groups_id": [],
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("user-mass-import"), {"users": [user_1, user_2]}, format="json"
|
reverse("user-mass-import"), {"users": [user_1, user_2]}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(User.objects.count(), 3)
|
self.assertEqual(User.objects.count(), 3)
|
||||||
@ -640,9 +628,7 @@ class UserSendIntivationEmail(TestCase):
|
|||||||
"subject": config["users_email_subject"],
|
"subject": config["users_email_subject"],
|
||||||
"message": config["users_email_body"],
|
"message": config["users_email_body"],
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(reverse("user-mass-invite-email"), data)
|
||||||
reverse("user-mass-invite-email"), data, format="json"
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.data["count"], 1)
|
self.assertEqual(response.data["count"], 1)
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
@ -666,6 +652,9 @@ class GroupMetadata(TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class GroupReceive(TestCase):
|
class GroupReceive(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_get_groups_as_anonymous_deactivated(self):
|
def test_get_groups_as_anonymous_deactivated(self):
|
||||||
"""
|
"""
|
||||||
Test to get the groups with an anonymous user, when they are deactivated.
|
Test to get the groups with an anonymous user, when they are deactivated.
|
||||||
@ -849,7 +838,6 @@ class GroupUpdate(TestCase):
|
|||||||
response = admin_client.put(
|
response = admin_client.put(
|
||||||
reverse("group-detail", args=[group.pk]),
|
reverse("group-detail", args=[group.pk]),
|
||||||
{"name": "new_group_name_Chie6duwaepoo8aech7r", "permissions": permissions},
|
{"name": "new_group_name_Chie6duwaepoo8aech7r", "permissions": permissions},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -869,7 +857,6 @@ class GroupUpdate(TestCase):
|
|||||||
response = admin_client.post(
|
response = admin_client.post(
|
||||||
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
||||||
{"perm": "users.can_manage", "set": True},
|
{"perm": "users.can_manage", "set": True},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -887,7 +874,6 @@ class GroupUpdate(TestCase):
|
|||||||
response = admin_client.post(
|
response = admin_client.post(
|
||||||
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
||||||
{"perm": "not_existing.permission", "set": True},
|
{"perm": "not_existing.permission", "set": True},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
@ -899,7 +885,6 @@ class GroupUpdate(TestCase):
|
|||||||
response = admin_client.post(
|
response = admin_client.post(
|
||||||
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
reverse("group-set-permission", args=[GROUP_DEFAULT_PK]),
|
||||||
{"perm": "users.can_see_name", "set": False},
|
{"perm": "users.can_see_name", "set": False},
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -972,7 +957,6 @@ class PersonalNoteTest(TestCase):
|
|||||||
{"collection": "example-model", "id": 1, "content": content1},
|
{"collection": "example-model", "id": 1, "content": content1},
|
||||||
{"collection": "example-model", "id": 2, "content": content2},
|
{"collection": "example-model", "id": 2, "content": content2},
|
||||||
],
|
],
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertTrue(PersonalNote.objects.exists())
|
self.assertTrue(PersonalNote.objects.exists())
|
||||||
@ -985,9 +969,7 @@ class PersonalNoteTest(TestCase):
|
|||||||
|
|
||||||
def test_anonymous_create(self):
|
def test_anonymous_create(self):
|
||||||
guest_client = APIClient()
|
guest_client = APIClient()
|
||||||
response = guest_client.post(
|
response = guest_client.post(reverse("personalnote-create-or-update"), [])
|
||||||
reverse("personalnote-create-or-update"), [], format="json"
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertFalse(PersonalNote.objects.exists())
|
self.assertFalse(PersonalNote.objects.exists())
|
||||||
|
|
||||||
@ -1007,7 +989,6 @@ class PersonalNoteTest(TestCase):
|
|||||||
"content": "test_note_do2ncoi7ci2fm93LjwlO",
|
"content": "test_note_do2ncoi7ci2fm93LjwlO",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
personal_note = PersonalNote.objects.get()
|
personal_note = PersonalNote.objects.get()
|
||||||
|
@ -2,7 +2,7 @@ from openslides.agenda.models import ListOfSpeakers, Speaker
|
|||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.test import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
class ListOfSpeakerModelTests(TestCase):
|
class ListOfSpeakerModelTests(TestCase):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user