Merge pull request #5344 from FinnStutzenstein/voteWeight
Removed vote weight from votes_cast
This commit is contained in:
commit
cce76118c3
@ -17,6 +17,11 @@ import { DataSendService } from '../../core-services/data-send.service';
|
|||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
|
export interface MassImportResult {
|
||||||
|
importedTrackIds: number[];
|
||||||
|
errors: { [id: number]: string };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* type for determining the user name from a string during import.
|
* type for determining the user name from a string during import.
|
||||||
* See {@link parseUserString} for implementations
|
* See {@link parseUserString} for implementations
|
||||||
@ -222,15 +227,11 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
*
|
*
|
||||||
* @param newEntries
|
* @param newEntries
|
||||||
*/
|
*/
|
||||||
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<number[]> {
|
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<MassImportResult> {
|
||||||
const data = newEntries.map(entry => {
|
const data = newEntries.map(entry => {
|
||||||
return { ...entry.newEntry, importTrackId: entry.importTrackId };
|
return { ...entry.newEntry, importTrackId: entry.importTrackId };
|
||||||
});
|
});
|
||||||
const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as {
|
return await this.httpService.post<MassImportResult>(`/rest/users/user/mass_import/`, { users: data });
|
||||||
detail: string;
|
|
||||||
importedTrackIds: number[];
|
|
||||||
};
|
|
||||||
return response.importedTrackIds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,7 +179,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all stored secondary data
|
* Clears all stored secondary data
|
||||||
* TODO: Merge with clearPreview()
|
|
||||||
*/
|
*/
|
||||||
public abstract clearData(): void;
|
public abstract clearData(): void;
|
||||||
|
|
||||||
@ -190,7 +189,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
public parseInput(file: string): void {
|
public parseInput(file: string): void {
|
||||||
this.clearData();
|
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
const papaConfig: ParseConfig = {
|
const papaConfig: ParseConfig = {
|
||||||
header: false,
|
header: false,
|
||||||
@ -205,28 +203,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entryLines.forEach(line => {
|
this._entries = entryLines.map(x => this.mapData(x)).filter(x => !!x);
|
||||||
const item = this.mapData(line);
|
|
||||||
if (item) {
|
|
||||||
this._entries.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.newEntries.next(this._entries);
|
|
||||||
this.updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* parses pre-prepared entries (e.g. from a textarea) instead of a csv structure
|
|
||||||
*
|
|
||||||
* @param entries: an array of prepared newEntry objects
|
|
||||||
*/
|
|
||||||
public setParsedEntries(entries: NewEntry<M>[]): void {
|
|
||||||
this.clearData();
|
|
||||||
this.clearPreview();
|
|
||||||
if (!entries) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._entries = entries;
|
|
||||||
this.newEntries.next(this._entries);
|
this.newEntries.next(this._entries);
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
}
|
}
|
||||||
@ -238,6 +215,21 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
*/
|
*/
|
||||||
public abstract mapData(line: string): NewEntry<M>;
|
public abstract mapData(line: string): NewEntry<M>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parses pre-prepared entries (e.g. from a textarea) instead of a csv structure
|
||||||
|
*
|
||||||
|
* @param entries: an array of prepared newEntry objects
|
||||||
|
*/
|
||||||
|
public setParsedEntries(entries: NewEntry<M>[]): void {
|
||||||
|
this.clearPreview();
|
||||||
|
if (!entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._entries = entries;
|
||||||
|
this.newEntries.next(this._entries);
|
||||||
|
this.updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger for executing the import.
|
* Trigger for executing the import.
|
||||||
*/
|
*/
|
||||||
@ -293,7 +285,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
// TODO: error message for wrong file type (test Firefox on Windows!)
|
// TODO: error message for wrong file type (test Firefox on Windows!)
|
||||||
if (event.target.files && event.target.files.length === 1) {
|
if (event.target.files && event.target.files.length === 1) {
|
||||||
this._rawFile = event.target.files[0];
|
this._rawFile = event.target.files[0];
|
||||||
this.readFile(event.target.files[0]);
|
this.readFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,15 +295,15 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
*/
|
*/
|
||||||
public refreshFile(): void {
|
public refreshFile(): void {
|
||||||
if (this._rawFile) {
|
if (this._rawFile) {
|
||||||
this.readFile(this._rawFile);
|
this.readFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (re)-reads a given file with the current parameter
|
* reads the _rawFile
|
||||||
*/
|
*/
|
||||||
private readFile(file: File): void {
|
private readFile(): void {
|
||||||
this.reader.readAsText(file, this.encoding);
|
this.reader.readAsText(this._rawFile, this.encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -349,6 +341,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* Resets the data and preview (triggered upon selecting an invalid file)
|
* Resets the data and preview (triggered upon selecting an invalid file)
|
||||||
*/
|
*/
|
||||||
public clearPreview(): void {
|
public clearPreview(): void {
|
||||||
|
this.clearData();
|
||||||
this._entries = [];
|
this._entries = [];
|
||||||
this.newEntries.next([]);
|
this.newEntries.next([]);
|
||||||
this._preview = null;
|
this._preview = null;
|
||||||
@ -358,7 +351,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* set a list of short names for error, indicating which column failed
|
* set a list of short names for error, indicating which column failed
|
||||||
*/
|
*/
|
||||||
public setError(entry: NewEntry<M>, error: string): void {
|
public setError(entry: NewEntry<M>, error: string): void {
|
||||||
if (this.errorList.hasOwnProperty(error)) {
|
if (this.errorList[error]) {
|
||||||
if (!entry.errors) {
|
if (!entry.errors) {
|
||||||
entry.errors = [error];
|
entry.errors = [error];
|
||||||
} else if (!entry.errors.includes(error)) {
|
} else if (!entry.errors.includes(error)) {
|
||||||
|
@ -141,7 +141,6 @@ export abstract class BaseImportListComponentDirective<M extends BaseModel> exte
|
|||||||
.getNewEntries()
|
.getNewEntries()
|
||||||
.pipe(auditTime(100))
|
.pipe(auditTime(100))
|
||||||
.subscribe(newEntries => {
|
.subscribe(newEntries => {
|
||||||
this.dataSource.data = [];
|
|
||||||
this.dataSource.data = newEntries;
|
this.dataSource.data = newEntries;
|
||||||
this.hasFile = newEntries.length > 0;
|
this.hasFile = newEntries.length > 0;
|
||||||
});
|
});
|
||||||
|
@ -51,7 +51,8 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
NoName: 'Entry has no valid name',
|
NoName: 'Entry has no valid name',
|
||||||
DuplicateImport: 'Entry cannot be imported twice. This line will be ommitted',
|
DuplicateImport: 'Entry cannot be imported twice. This line will be ommitted',
|
||||||
ParsingErrors: 'Some csv values could not be read correctly.',
|
ParsingErrors: 'Some csv values could not be read correctly.',
|
||||||
FailedImport: 'Imported user could not be imported.'
|
FailedImport: 'Imported user could not be imported.',
|
||||||
|
vote_weight: 'The vote weight has too many decimal places (max.: 6).'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,19 +95,19 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
* @returns a new entry representing an User
|
* @returns a new entry representing an User
|
||||||
*/
|
*/
|
||||||
public mapData(line: string): NewEntry<User> {
|
public mapData(line: string): NewEntry<User> {
|
||||||
const newViewUser = new ImportCreateUser();
|
const user = new ImportCreateUser();
|
||||||
const headerLength = Math.min(this.expectedHeader.length, line.length);
|
const headerLength = Math.min(this.expectedHeader.length, line.length);
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
for (let idx = 0; idx < headerLength; idx++) {
|
for (let idx = 0; idx < headerLength; idx++) {
|
||||||
switch (this.expectedHeader[idx]) {
|
switch (this.expectedHeader[idx]) {
|
||||||
case 'groups_id':
|
case 'groups_id':
|
||||||
newViewUser.csvGroups = this.getGroups(line[idx]);
|
user.csvGroups = this.getGroups(line[idx]);
|
||||||
break;
|
break;
|
||||||
case 'is_active':
|
case 'is_active':
|
||||||
case 'is_committee':
|
case 'is_committee':
|
||||||
case 'is_present':
|
case 'is_present':
|
||||||
try {
|
try {
|
||||||
newViewUser[this.expectedHeader[idx]] = this.toBoolean(line[idx]);
|
user[this.expectedHeader[idx]] = this.toBoolean(line[idx]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof TypeError) {
|
if (e instanceof TypeError) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -116,21 +117,21 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'number':
|
case 'number':
|
||||||
newViewUser.number = line[idx];
|
user.number = line[idx];
|
||||||
break;
|
break;
|
||||||
case 'vote_weight':
|
case 'vote_weight':
|
||||||
if (!line[idx]) {
|
if (!line[idx]) {
|
||||||
newViewUser[this.expectedHeader[idx]] = 1;
|
user[this.expectedHeader[idx]] = 1;
|
||||||
} else {
|
} else {
|
||||||
newViewUser[this.expectedHeader[idx]] = line[idx];
|
user[this.expectedHeader[idx]] = line[idx];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
newViewUser[this.expectedHeader[idx]] = line[idx];
|
user[this.expectedHeader[idx]] = line[idx];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newEntry = this.userToEntry(newViewUser);
|
const newEntry = this.userToEntry(user);
|
||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
this.setError(newEntry, 'ParsingErrors');
|
this.setError(newEntry, 'ParsingErrors');
|
||||||
}
|
}
|
||||||
@ -151,8 +152,8 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
if (entry.status !== 'new') {
|
if (entry.status !== 'new') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const openBlocks = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups);
|
const openGroups = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups);
|
||||||
if (openBlocks) {
|
if (openGroups) {
|
||||||
this.setError(entry, 'Group');
|
this.setError(entry, 'Group');
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
continue;
|
continue;
|
||||||
@ -163,13 +164,15 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
}
|
}
|
||||||
while (importUsers.length) {
|
while (importUsers.length) {
|
||||||
const subSet = importUsers.splice(0, 100); // don't send bulks too large
|
const subSet = importUsers.splice(0, 100); // don't send bulks too large
|
||||||
const importedTracks = await this.repo.bulkCreate(subSet);
|
const result = await this.repo.bulkCreate(subSet);
|
||||||
subSet.map(entry => {
|
subSet.forEach(importUser => {
|
||||||
const importModel = this.entries.find(e => e.importTrackId === entry.importTrackId);
|
// const importModel = this.entries.find(e => e.importTrackId === importUser.importTrackId);
|
||||||
if (importModel && importedTracks.includes(importModel.importTrackId)) {
|
if (importUser && result.importedTrackIds.includes(importUser.importTrackId)) {
|
||||||
importModel.status = 'done';
|
importUser.status = 'done';
|
||||||
|
} else if (result.errors[importUser.importTrackId]) {
|
||||||
|
this.setError(importUser, result.errors[importUser.importTrackId]);
|
||||||
} else {
|
} else {
|
||||||
this.setError(importModel, 'FailedImport');
|
this.setError(importUser, 'FailedImport');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
@ -273,20 +276,20 @@ export class UserImportService extends BaseImportService<User> {
|
|||||||
/**
|
/**
|
||||||
* Checks a newly created ViewCsvCreateuser for validity and duplicates,
|
* Checks a newly created ViewCsvCreateuser for validity and duplicates,
|
||||||
*
|
*
|
||||||
* @param newUser
|
* @param user
|
||||||
* @returns a NewEntry with duplicate/error information
|
* @returns a NewEntry with duplicate/error information
|
||||||
*/
|
*/
|
||||||
private userToEntry(newUser: ImportCreateUser): NewEntry<User> {
|
private userToEntry(user: ImportCreateUser): NewEntry<User> {
|
||||||
const newEntry: NewEntry<User> = {
|
const newEntry: NewEntry<User> = {
|
||||||
newEntry: newUser,
|
newEntry: user,
|
||||||
hasDuplicates: false,
|
hasDuplicates: false,
|
||||||
status: 'new',
|
status: 'new',
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
if (newUser.isValid) {
|
if (user.isValid) {
|
||||||
newEntry.hasDuplicates = this.repo
|
newEntry.hasDuplicates = this.repo
|
||||||
.getViewModelList()
|
.getViewModelList()
|
||||||
.some(user => user.full_name === this.repo.getFullName(newUser));
|
.some(existingUser => existingUser.full_name === this.repo.getFullName(user));
|
||||||
if (newEntry.hasDuplicates) {
|
if (newEntry.hasDuplicates) {
|
||||||
this.setError(newEntry, 'Duplicates');
|
this.setError(newEntry, 'Duplicates');
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ class BasePoll(models.Model):
|
|||||||
if self.type == self.TYPE_ANALOG:
|
if self.type == self.TYPE_ANALOG:
|
||||||
return self.db_votescast
|
return self.db_votescast
|
||||||
else:
|
else:
|
||||||
return Decimal(self.amount_users_voted_with_individual_weight())
|
return Decimal(self.amount_users_voted())
|
||||||
|
|
||||||
def set_votescast(self, value):
|
def set_votescast(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -220,11 +220,14 @@ class BasePoll(models.Model):
|
|||||||
|
|
||||||
votescast = property(get_votescast, set_votescast)
|
votescast = property(get_votescast, set_votescast)
|
||||||
|
|
||||||
|
def amount_users_voted(self):
|
||||||
|
return len(self.voted.all())
|
||||||
|
|
||||||
def amount_users_voted_with_individual_weight(self):
|
def amount_users_voted_with_individual_weight(self):
|
||||||
if config["users_activate_vote_weight"]:
|
if config["users_activate_vote_weight"]:
|
||||||
return sum(user.vote_weight for user in self.voted.all())
|
return sum(user.vote_weight for user in self.voted.all())
|
||||||
else:
|
else:
|
||||||
return len(self.voted.all())
|
return self.amount_users_voted()
|
||||||
|
|
||||||
def create_options(self):
|
def create_options(self):
|
||||||
""" Should be called after creation of this model. """
|
""" Should be called after creation of this model. """
|
||||||
|
@ -354,14 +354,16 @@ class UserViewSet(ModelViewSet):
|
|||||||
created_users = []
|
created_users = []
|
||||||
# List of all track ids of all imported users. The track ids are just used in the client.
|
# List of all track ids of all imported users. The track ids are just used in the client.
|
||||||
imported_track_ids = []
|
imported_track_ids = []
|
||||||
|
errors = {} # maps imported track ids to errors
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
serializer = self.get_serializer(data=user)
|
serializer = self.get_serializer(data=user)
|
||||||
try:
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
except ValidationError:
|
except ValidationError as e:
|
||||||
# Skip invalid users.
|
# Skip invalid users.
|
||||||
|
if "vote_weight" in e.detail and "importTrackId" in user:
|
||||||
|
errors[user["importTrackId"]] = "vote_weight"
|
||||||
continue
|
continue
|
||||||
data = serializer.prepare_password(serializer.data)
|
data = serializer.prepare_password(serializer.data)
|
||||||
groups = data["groups_id"]
|
groups = data["groups_id"]
|
||||||
@ -383,6 +385,7 @@ class UserViewSet(ModelViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"detail": "{0} users successfully imported.",
|
"detail": "{0} users successfully imported.",
|
||||||
|
"errors": errors,
|
||||||
"args": [len(created_users)],
|
"args": [len(created_users)],
|
||||||
"importedTrackIds": imported_track_ids,
|
"importedTrackIds": imported_track_ids,
|
||||||
}
|
}
|
||||||
|
@ -1013,7 +1013,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
self.assertEqual(poll.votesvalid, weight)
|
self.assertEqual(poll.votesvalid, weight)
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, weight)
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
||||||
option1 = poll.options.get(pk=1)
|
option1 = poll.options.get(pk=1)
|
||||||
|
@ -779,7 +779,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.votesvalid, weight)
|
self.assertEqual(poll.votesvalid, weight)
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, weight)
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.get_votes().count(), 1)
|
self.assertEqual(poll.get_votes().count(), 1)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
|
Loading…
Reference in New Issue
Block a user