Merge pull request #4541 from FinnStutzenstein/projectionDefaults2

Projectiondefaults, width slider direct save
This commit is contained in:
Finn Stutzenstein 2019-04-02 12:30:20 +02:00 committed by GitHub
commit 29762ad89e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 269 additions and 53 deletions

View File

@ -34,7 +34,7 @@ matrix:
script:
- flake8 openslides tests
- isort --check-only --diff --recursive openslides tests
- black --check --diff --py36 openslides tests
- black --check --diff --target-version py36 openslides tests
- python -m mypy openslides/ tests/
- python -W ignore -m pytest --cov --cov-fail-under=70

View File

@ -23,6 +23,7 @@ import { ViewModelStoreService } from './view-model-store.service';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { ConfigService } from '../ui-services/config.service';
import { ProjectorDataService } from './projector-data.service';
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
/**
* This service cares about Projectables being projected and manage all projection-related
@ -254,10 +255,13 @@ export class ProjectorService {
* @param projectiondefault The projection default
* @return the projector associated to the given projectiondefault.
*/
public getProjectorForDefault(projectiondefault: string): Projector {
return this.DS.getAll<Projector>('core/projector').find(projector => {
return projector.projectiondefaults.map(pd => pd.name).includes(projectiondefault);
});
public getProjectorForDefault(projectiondefault: string): Projector | null {
const pd = this.DS.find(ProjectionDefault, _pd => _pd.name === projectiondefault);
if (pd) {
return this.DS.get<Projector>(Projector, pd.projector_id);
} else {
return null;
}
}
/**

View File

@ -3,7 +3,7 @@ import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { CountdownRepositoryService } from './countdown-repository.service';
describe('StatuteParagraphRepositoryService', () => {
describe('CountdownRepositoryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],

View File

@ -1,4 +1,7 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
@ -6,7 +9,6 @@ import { CollectionStringMapperService } from '../../core-services/collectionStr
import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Countdown } from 'app/shared/models/core/countdown';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core';
import { ServertimeService } from 'app/core/core-services/servertime.service';
@Injectable({
@ -34,15 +36,31 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
return viewCountdown;
}
/**
* Starts a countdown.
*
* @param countdown The countdown to start.
*/
public async start(countdown: ViewCountdown): Promise<void> {
const endTime = this.servertimeService.getServertime() / 1000 + countdown.countdown_time;
await this.update({ running: true, countdown_time: endTime }, countdown);
}
/**
* Stops (former `reset`) a countdown. Sets the countdown time to the default time. If
* this should not happen, use `pause()`.
*
* @param countdown The countdown to stop.
*/
public async stop(countdown: ViewCountdown): Promise<void> {
await this.update({ running: false, countdown_time: countdown.default_time }, countdown);
}
/**
* Pauses the countdown. The remaining time will stay.
*
* @param countdown The countdown to pause.
*/
public async pause(countdown: ViewCountdown): Promise<void> {
const endTime = countdown.countdown_time - this.servertimeService.getServertime() / 1000;
await this.update({ running: false, countdown_time: endTime }, countdown);

View File

@ -0,0 +1,20 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from '../../../../e2e-imports.module';
import { ProjectionDefaultRepositoryService } from './projection-default-repository.service';
describe('ProjectionDefaultRepositoryService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [ProjectionDefaultRepositoryService]
})
);
it('should be created', inject(
[ProjectionDefaultRepositoryService],
(service: ProjectionDefaultRepositoryService) => {
expect(service).toBeTruthy();
}
));
});

View File

@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
import { ViewProjectionDefault } from 'app/site/projector/models/view-projection-default';
/**
* Manages all projection default instances.
*/
@Injectable({
providedIn: 'root'
})
export class ProjectionDefaultRepositoryService extends BaseRepository<ViewProjectionDefault, ProjectionDefault> {
/**
* Constructor calls the parent constructor
*
* @param DS The DataStore
* @param dataSend sending changed objects
* @param mapperService Maps collection strings to classes
* @param viewModelStoreService
* @param translate
*/
public constructor(
DS: DataStoreService,
dataSend: DataSendService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
translate: TranslateService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, ProjectionDefault);
}
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Projectiondefaults' : 'Projectiondefault');
};
public getTitle = (projectionDefault: Partial<ProjectionDefault> | Partial<ViewProjectionDefault>) => {
return this.translate.instant(projectionDefault.display_name);
};
public createViewModel(projectionDefault: ProjectionDefault): ViewProjectionDefault {
const viewProjectionDefault = new ViewProjectionDefault(projectionDefault);
viewProjectionDefault.getVerboseName = this.getVerboseName;
viewProjectionDefault.getTitle = () => this.getTitle(viewProjectionDefault);
return viewProjectionDefault;
}
/**
* Creation of projection defaults is not supported.
*/
public async create(projectorData: Partial<ProjectionDefault>): Promise<Identifiable> {
throw new Error('Not supported');
}
/**
* Deletion of projection defaults is not supported.
*/
public async delete(viewProjectionDefault: ViewProjectionDefault): Promise<void> {
throw new Error('Not supported');
}
}

View File

@ -3,7 +3,7 @@ import { TestBed, inject } from '@angular/core/testing';
import { ProjectorRepositoryService } from './projector-repository.service';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('GroupRepositoryService', () => {
describe('ProjectorRepositoryService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule],

View File

@ -47,7 +47,7 @@ export class ProjectionDialogComponent {
const defaultProjector: Projector = this.projectorService.getProjectorForDefault(
this.projectorElementBuildDescriptor.projectionDefaultName
);
if (!this.selectedProjectors.includes(defaultProjector)) {
if (defaultProjector && !this.selectedProjectors.includes(defaultProjector)) {
this.selectedProjectors.push(defaultProjector);
}
}

View File

@ -0,0 +1,19 @@
import { BaseModel } from '../base/base-model';
/**
* Representation of a projection default
*
* @ignore
*/
export class ProjectionDefault extends BaseModel<ProjectionDefault> {
public static COLLECTIONSTRING = 'core/projection-default';
public id: number;
public name: string;
public display_name: string;
public projector_id: number;
public constructor(input?: any) {
super(ProjectionDefault.COLLECTIONSTRING, input);
}
}

View File

@ -48,17 +48,7 @@ export function elementIdentifies(a: IdentifiableProjectorElement, b: ProjectorE
export type ProjectorElements = ProjectorElement[];
/**
* A projectiondefault
*/
export interface ProjectionDefault {
id: number;
name: string;
display_name: string;
projector_id: number;
}
/**
* Representation of a projector. Has the nested property "projectiondefaults"
* Representation of a projector.
*
* TODO: Move all function to the viewprojector.
*
@ -77,7 +67,7 @@ export class Projector extends BaseModel<Projector> {
public width: number;
public height: number;
public reference_projector_id: number;
public projectiondefaults: ProjectionDefault[];
public projectiondefaults_id: number[];
public background_color: string;
public header_background_color: string;
public header_font_color: string;

View File

@ -104,9 +104,18 @@
min="800"
max="3840"
step="10"
(change)="widthSliderValueChanged(projector, $event)"
></mat-slider>
{{ updateForm.value.width }}
<!-- projection defaults -->
<h3 translate>Projectiondefaults</h3>
<mat-select formControlName="projectiondefaults_id" placeholder="{{ 'Projectiondefaults' | translate }}" [multiple]="true">
<mat-option *ngFor="let pd of projectionDefaults" [value]="pd.id">
{{ pd.getTitle() | translate }}
</mat-option>
</mat-select>
<!-- colors -->
<mat-form-field>
<span translate>Background color</span>

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatSelectChange } from '@angular/material';
import { MatSnackBar, MatSelectChange, MatSliderChange } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
@ -12,6 +12,8 @@ import { BaseViewComponent } from 'app/site/base/base-view';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ClockSlideService } from '../../services/clock-slide.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
/**
* All supported aspect rations for projectors.
@ -62,6 +64,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
*/
public projectors: ViewProjector[];
public projectionDefaults: ViewProjectionDefault[];
/**
* Helper to check manage permissions
*
@ -91,7 +95,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
private formBuilder: FormBuilder,
private promptService: PromptService,
private clockSlideService: ClockSlideService,
private operator: OperatorService
private operator: OperatorService,
private projectionDefaultRepo: ProjectionDefaultRepositoryService
) {
super(titleService, translate, matSnackBar);
@ -104,6 +109,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
name: ['', Validators.required],
aspectRatio: ['', Validators.required],
width: [0, Validators.required],
projectiondefaults_id: [[]],
clock: [true],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
@ -122,6 +128,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
super.setTitle('Projectors');
this.projectors = this.repo.getViewModelList();
this.repo.getViewModelListObservable().subscribe(projectors => (this.projectors = projectors));
this.projectionDefaults = this.projectionDefaultRepo.getViewModelList();
this.projectionDefaultRepo.getViewModelListObservable().subscribe(pds => (this.projectionDefaults = pds));
}
/**
@ -271,4 +279,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
});
Promise.all(promises).then(null, this.raiseError);
}
public widthSliderValueChanged(projector: ViewProjector, event: MatSliderChange): void {
const aspectRatio = this.getAspectRatioKey(projector);
const updateProjector: Partial<Projector> = {
width: event.value
};
updateProjector.height = Math.round(event.value / aspectRatios[aspectRatio]);
this.repo.update(updateProjector, projector).then(null, this.raiseError);
}
}

View File

@ -0,0 +1,41 @@
import { BaseViewModel } from '../../base/base-view-model';
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
export class ViewProjectionDefault extends BaseViewModel {
public static COLLECTIONSTRING = ProjectionDefault.COLLECTIONSTRING;
private _projectionDefault: ProjectionDefault;
public get projectionDefault(): ProjectionDefault {
return this._projectionDefault;
}
public get id(): number {
return this.projectionDefault.id;
}
public get name(): string {
return this.projectionDefault.name;
}
public get display_name(): string {
return this.projectionDefault.display_name;
}
/**
* This is set by the repository
*/
public getVerboseName: () => string;
public getTitle: () => string;
public constructor(projectionDefault: ProjectionDefault) {
super(ProjectionDefault.COLLECTIONSTRING);
this._projectionDefault = projectionDefault;
}
public getModel(): ProjectionDefault {
return this.projectionDefault;
}
public updateDependencies(update: BaseViewModel): void {}
}

View File

@ -27,6 +27,10 @@ export class ViewProjector extends BaseViewModel {
return this.projector.name;
}
public get projectiondefaults_id(): number[] {
return this.projector.projectiondefaults_id;
}
public get elements(): ProjectorElements {
return this.projector.elements;
}

View File

@ -8,6 +8,9 @@ import { ProjectorMessageRepositoryService } from 'app/core/repositories/project
import { ViewProjector } from './models/view-projector';
import { ViewCountdown } from './models/view-countdown';
import { ViewProjectorMessage } from './models/view-projector-message';
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
import { ViewProjectionDefault } from './models/view-projection-default';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
export const ProjectorAppConfig: AppConfig = {
name: 'projector',
@ -18,6 +21,12 @@ export const ProjectorAppConfig: AppConfig = {
viewModel: ViewProjector,
repository: ProjectorRepositoryService
},
{
collectionString: 'core/projection-default',
model: ProjectionDefault,
viewModel: ViewProjectionDefault,
repository: ProjectionDefaultRepositoryService
},
{
collectionString: 'core/countdown',
model: Countdown,

View File

@ -80,4 +80,4 @@ def clean(args=None):
@command("format", help="Format code with isort and black")
def isort(args=None):
call("isort --recursive openslides tests")
call("black --py36 openslides tests")
call("black --target-version py36 openslides tests")

View File

@ -9,6 +9,14 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
base_permission = "core.can_see_projector"
class ProjectionDefaultAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Projector and ProjectorViewSet.
"""
base_permission = "core.can_see_projector"
class TagAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Tag and TagViewSet.

View File

@ -33,6 +33,7 @@ class CoreAppConfig(AppConfig):
HistoryViewSet,
ProjectorMessageViewSet,
ProjectorViewSet,
ProjectionDefaultViewSet,
TagViewSet,
)
from .websocket import (
@ -74,6 +75,10 @@ class CoreAppConfig(AppConfig):
router.register(
self.get_model("Projector").get_collection_string(), ProjectorViewSet
)
router.register(
self.get_model("Projectiondefault").get_collection_string(),
ProjectionDefaultViewSet,
)
router.register(
self.get_model("ChatMessage").get_collection_string(), ChatMessageViewSet
)
@ -121,6 +126,7 @@ class CoreAppConfig(AppConfig):
"""
for model_name in (
"Projector",
"ProjectionDefault",
"ChatMessage",
"Tag",
"ProjectorMessage",

View File

@ -16,6 +16,7 @@ from .access_permissions import (
ConfigAccessPermissions,
CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions,
@ -123,6 +124,8 @@ class ProjectionDefault(RESTModelMixin, models.Model):
name on the front end for the user.
"""
access_permissions = ProjectionDefaultAccessPermissions()
name = models.CharField(max_length=256)
display_name = models.CharField(max_length=256)
@ -131,9 +134,6 @@ class ProjectionDefault(RESTModelMixin, models.Model):
Projector, on_delete=models.PROTECT, related_name="projectiondefaults"
)
def get_root_rest_element(self):
return self.projector
class Meta:
default_permissions = ()

View File

@ -81,7 +81,6 @@ class ProjectorSerializer(ModelSerializer):
elements_preview = JSONSerializerField(validators=[elements_validator])
elements_history = JSONSerializerField(validators=[elements_array_validator])
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
width = IntegerField(min_value=800, max_value=3840, required=False)
height = IntegerField(min_value=340, max_value=2880, required=False)

View File

@ -39,6 +39,7 @@ from .access_permissions import (
ConfigAccessPermissions,
CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions,
@ -143,12 +144,18 @@ class ProjectorViewSet(ModelViewSet):
REST API operation for DELETE requests.
Assigns all ProjectionDefault objects from this projector to the
default projector (pk=1).
first projector found.
"""
if len(Projector.objects.all()) <= 1:
raise ValidationError({"detail": "You can't delete the last projector."})
projector_instance = self.get_object()
new_projector_id = (
Projector.objects.exclude(pk=projector_instance.pk).first().pk
)
for projection_default in ProjectionDefault.objects.all():
if projection_default.projector.id == projector_instance.id:
projection_default.projector_id = 1
projection_default.projector_id = new_projector_id
projection_default.save()
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
@ -272,32 +279,29 @@ class ProjectorViewSet(ModelViewSet):
message = f"Setting scroll to {request.data} was successful."
return Response({"detail": message})
@detail_route(methods=["post"])
def set_projectiondefault(self, request, pk):
"""
REST API operation to set a projectiondefault to the requested projector. The argument
has to be an int representing the pk from the projectiondefault to be set.
It expects a POST request to
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
"""
if not isinstance(request.data, int):
raise ValidationError({"detail": "Data must be an int."})
class ProjectionDefaultViewSet(ModelViewSet):
"""
API endpoint for projection defaults.
try:
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
except ProjectionDefault.DoesNotExist:
raise ValidationError(
{
"detail": f"The projectiondefault with pk={request.data} was not found."
}
)
There are the following views: list, retrieve, create, update,
partial_update and destroy.
"""
access_permissions = ProjectionDefaultAccessPermissions()
queryset = ProjectionDefault.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ("list", "retrieve"):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ("create", "partial_update", "update", "destroy"):
result = has_perm(self.request.user, "core.can_manage_projector")
else:
projector_instance = self.get_object()
projectiondefault.projector = projector_instance
projectiondefault.save()
return Response()
result = False
return result
class TagViewSet(ModelViewSet):