diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index 67ecc2f8b..db2f865b7 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -17,6 +17,11 @@ import { DataSendService } from '../../core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; 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. * See {@link parseUserString} for implementations @@ -222,15 +227,11 @@ export class UserRepositoryService extends BaseRepository[]): Promise { + public async bulkCreate(newEntries: NewEntry[]): Promise { const data = newEntries.map(entry => { return { ...entry.newEntry, importTrackId: entry.importTrackId }; }); - const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as { - detail: string; - importedTrackIds: number[]; - }; - return response.importedTrackIds; + return await this.httpService.post(`/rest/users/user/mass_import/`, { users: data }); } /** diff --git a/client/src/app/core/ui-services/base-import.service.ts b/client/src/app/core/ui-services/base-import.service.ts index af2c875ce..5ef1a9791 100644 --- a/client/src/app/core/ui-services/base-import.service.ts +++ b/client/src/app/core/ui-services/base-import.service.ts @@ -179,7 +179,6 @@ export abstract class BaseImportService { /** * Clears all stored secondary data - * TODO: Merge with clearPreview() */ public abstract clearData(): void; @@ -190,7 +189,6 @@ export abstract class BaseImportService { * @param file */ public parseInput(file: string): void { - this.clearData(); this.clearPreview(); const papaConfig: ParseConfig = { header: false, @@ -205,28 +203,7 @@ export abstract class BaseImportService { if (!valid) { return; } - entryLines.forEach(line => { - 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[]): void { - this.clearData(); - this.clearPreview(); - if (!entries) { - return; - } - this._entries = entries; + this._entries = entryLines.map(x => this.mapData(x)).filter(x => !!x); this.newEntries.next(this._entries); this.updatePreview(); } @@ -238,6 +215,21 @@ export abstract class BaseImportService { */ public abstract mapData(line: string): NewEntry; + /** + * 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[]): void { + this.clearPreview(); + if (!entries) { + return; + } + this._entries = entries; + this.newEntries.next(this._entries); + this.updatePreview(); + } + /** * Trigger for executing the import. */ @@ -293,7 +285,7 @@ export abstract class BaseImportService { // TODO: error message for wrong file type (test Firefox on Windows!) if (event.target.files && event.target.files.length === 1) { this._rawFile = event.target.files[0]; - this.readFile(event.target.files[0]); + this.readFile(); } } @@ -303,15 +295,15 @@ export abstract class BaseImportService { */ public refreshFile(): void { 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 { - this.reader.readAsText(file, this.encoding); + private readFile(): void { + this.reader.readAsText(this._rawFile, this.encoding); } /** @@ -349,6 +341,7 @@ export abstract class BaseImportService { * Resets the data and preview (triggered upon selecting an invalid file) */ public clearPreview(): void { + this.clearData(); this._entries = []; this.newEntries.next([]); this._preview = null; @@ -358,7 +351,7 @@ export abstract class BaseImportService { * set a list of short names for error, indicating which column failed */ public setError(entry: NewEntry, error: string): void { - if (this.errorList.hasOwnProperty(error)) { + if (this.errorList[error]) { if (!entry.errors) { entry.errors = [error]; } else if (!entry.errors.includes(error)) { diff --git a/client/src/app/site/base/base-import-list.ts b/client/src/app/site/base/base-import-list.ts index d47f6aa94..e783ca690 100644 --- a/client/src/app/site/base/base-import-list.ts +++ b/client/src/app/site/base/base-import-list.ts @@ -141,7 +141,6 @@ export abstract class BaseImportListComponentDirective exte .getNewEntries() .pipe(auditTime(100)) .subscribe(newEntries => { - this.dataSource.data = []; this.dataSource.data = newEntries; this.hasFile = newEntries.length > 0; }); diff --git a/client/src/app/site/users/services/user-import.service.ts b/client/src/app/site/users/services/user-import.service.ts index 6e419f87d..cdcf194e9 100644 --- a/client/src/app/site/users/services/user-import.service.ts +++ b/client/src/app/site/users/services/user-import.service.ts @@ -51,7 +51,8 @@ export class UserImportService extends BaseImportService { NoName: 'Entry has no valid name', DuplicateImport: 'Entry cannot be imported twice. This line will be ommitted', 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 { * @returns a new entry representing an User */ public mapData(line: string): NewEntry { - const newViewUser = new ImportCreateUser(); + const user = new ImportCreateUser(); const headerLength = Math.min(this.expectedHeader.length, line.length); let hasErrors = false; for (let idx = 0; idx < headerLength; idx++) { switch (this.expectedHeader[idx]) { case 'groups_id': - newViewUser.csvGroups = this.getGroups(line[idx]); + user.csvGroups = this.getGroups(line[idx]); break; case 'is_active': case 'is_committee': case 'is_present': try { - newViewUser[this.expectedHeader[idx]] = this.toBoolean(line[idx]); + user[this.expectedHeader[idx]] = this.toBoolean(line[idx]); } catch (e) { if (e instanceof TypeError) { console.log(e); @@ -116,21 +117,21 @@ export class UserImportService extends BaseImportService { } break; case 'number': - newViewUser.number = line[idx]; + user.number = line[idx]; break; case 'vote_weight': if (!line[idx]) { - newViewUser[this.expectedHeader[idx]] = 1; + user[this.expectedHeader[idx]] = 1; } else { - newViewUser[this.expectedHeader[idx]] = line[idx]; + user[this.expectedHeader[idx]] = line[idx]; } break; default: - newViewUser[this.expectedHeader[idx]] = line[idx]; + user[this.expectedHeader[idx]] = line[idx]; break; } } - const newEntry = this.userToEntry(newViewUser); + const newEntry = this.userToEntry(user); if (hasErrors) { this.setError(newEntry, 'ParsingErrors'); } @@ -151,8 +152,8 @@ export class UserImportService extends BaseImportService { if (entry.status !== 'new') { continue; } - const openBlocks = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups); - if (openBlocks) { + const openGroups = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups); + if (openGroups) { this.setError(entry, 'Group'); this.updatePreview(); continue; @@ -163,13 +164,15 @@ export class UserImportService extends BaseImportService { } while (importUsers.length) { const subSet = importUsers.splice(0, 100); // don't send bulks too large - const importedTracks = await this.repo.bulkCreate(subSet); - subSet.map(entry => { - const importModel = this.entries.find(e => e.importTrackId === entry.importTrackId); - if (importModel && importedTracks.includes(importModel.importTrackId)) { - importModel.status = 'done'; + const result = await this.repo.bulkCreate(subSet); + subSet.forEach(importUser => { + // const importModel = this.entries.find(e => e.importTrackId === importUser.importTrackId); + if (importUser && result.importedTrackIds.includes(importUser.importTrackId)) { + importUser.status = 'done'; + } else if (result.errors[importUser.importTrackId]) { + this.setError(importUser, result.errors[importUser.importTrackId]); } else { - this.setError(importModel, 'FailedImport'); + this.setError(importUser, 'FailedImport'); } }); this.updatePreview(); @@ -273,20 +276,20 @@ export class UserImportService extends BaseImportService { /** * Checks a newly created ViewCsvCreateuser for validity and duplicates, * - * @param newUser + * @param user * @returns a NewEntry with duplicate/error information */ - private userToEntry(newUser: ImportCreateUser): NewEntry { + private userToEntry(user: ImportCreateUser): NewEntry { const newEntry: NewEntry = { - newEntry: newUser, + newEntry: user, hasDuplicates: false, status: 'new', errors: [] }; - if (newUser.isValid) { + if (user.isValid) { newEntry.hasDuplicates = this.repo .getViewModelList() - .some(user => user.full_name === this.repo.getFullName(newUser)); + .some(existingUser => existingUser.full_name === this.repo.getFullName(user)); if (newEntry.hasDuplicates) { this.setError(newEntry, 'Duplicates'); } diff --git a/openslides/poll/models.py b/openslides/poll/models.py index 555d0494b..058550544 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -211,7 +211,7 @@ class BasePoll(models.Model): if self.type == self.TYPE_ANALOG: return self.db_votescast else: - return Decimal(self.amount_users_voted_with_individual_weight()) + return Decimal(self.amount_users_voted()) def set_votescast(self, value): if self.type != self.TYPE_ANALOG: @@ -220,11 +220,14 @@ class BasePoll(models.Model): votescast = property(get_votescast, set_votescast) + def amount_users_voted(self): + return len(self.voted.all()) + def amount_users_voted_with_individual_weight(self): if config["users_activate_vote_weight"]: return sum(user.vote_weight for user in self.voted.all()) else: - return len(self.voted.all()) + return self.amount_users_voted() def create_options(self): """ Should be called after creation of this model. """ diff --git a/openslides/users/views.py b/openslides/users/views.py index 9b09e92ad..cef80717d 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -354,14 +354,16 @@ class UserViewSet(ModelViewSet): created_users = [] # List of all track ids of all imported users. The track ids are just used in the client. imported_track_ids = [] + errors = {} # maps imported track ids to errors for user in users: serializer = self.get_serializer(data=user) try: serializer.is_valid(raise_exception=True) - except ValidationError: + except ValidationError as e: # Skip invalid users. - + if "vote_weight" in e.detail and "importTrackId" in user: + errors[user["importTrackId"]] = "vote_weight" continue data = serializer.prepare_password(serializer.data) groups = data["groups_id"] @@ -383,6 +385,7 @@ class UserViewSet(ModelViewSet): return Response( { "detail": "{0} users successfully imported.", + "errors": errors, "args": [len(created_users)], "importedTrackIds": imported_track_ids, } diff --git a/tests/integration/assignments/test_polls.py b/tests/integration/assignments/test_polls.py index ead52ca7a..aa1a07257 100644 --- a/tests/integration/assignments/test_polls.py +++ b/tests/integration/assignments/test_polls.py @@ -1013,7 +1013,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): poll = AssignmentPoll.objects.get() self.assertEqual(poll.votesvalid, weight) 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.amount_users_voted_with_individual_weight(), weight) option1 = poll.options.get(pk=1) diff --git a/tests/integration/motions/test_polls.py b/tests/integration/motions/test_polls.py index 3c2b46a91..8714a103c 100644 --- a/tests/integration/motions/test_polls.py +++ b/tests/integration/motions/test_polls.py @@ -779,7 +779,7 @@ class VoteMotionPollNamed(TestCase): poll = MotionPoll.objects.get() self.assertEqual(poll.votesvalid, weight) 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.amount_users_voted_with_individual_weight(), weight) option = poll.options.get()