Merge pull request #5344 from FinnStutzenstein/voteWeight

Removed vote weight from votes_cast
This commit is contained in:
Emanuel Schütze 2020-04-30 10:54:28 +02:00 committed by GitHub
commit cce76118c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 67 additions and 65 deletions

View File

@ -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;
} }
/** /**

View File

@ -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)) {

View File

@ -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;
}); });

View File

@ -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');
} }

View File

@ -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. """

View File

@ -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,
} }

View File

@ -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)

View File

@ -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()