SKE-67 SKE-68 user profile
This commit is contained in:
parent
7cf0245b43
commit
5565bb7070
@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppRouteGuard } from '@shared/auth/auth-route-guard';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { ProfileComponent } from './profile/profile.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { UsersComponent } from './users/users.component';
|
||||
import { TenantsComponent } from './tenants/tenants.component';
|
||||
@ -20,6 +21,7 @@ import { CompetitionCreateComponent } from '@app/competition-create/competition-
|
||||
component: AppComponent,
|
||||
children: [
|
||||
{ path: 'home', component: HomeComponent, canActivate: [AppRouteGuard] },
|
||||
{ path: 'profile', component: ProfileComponent, canActivate: [AppRouteGuard] },
|
||||
{ path: 'users', component: UsersComponent, data: { permission: 'Pages.Users' }, canActivate: [AppRouteGuard] },
|
||||
{ path: 'roles', component: RolesComponent, data: { permission: 'Pages.Roles' }, canActivate: [AppRouteGuard] },
|
||||
{ path: 'tenants', component: TenantsComponent, data: { permission: 'Pages.Tenants' }, canActivate: [AppRouteGuard] },
|
||||
|
@ -16,6 +16,7 @@ import { ServiceProxyModule } from '@shared/service-proxies/service-proxy.module
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
|
||||
import { HomeComponent } from '@app/home/home.component';
|
||||
import { ProfileComponent } from '@app/profile/profile.component';
|
||||
import { AboutComponent } from '@app/about/about.component';
|
||||
import { UsersComponent } from '@app/users/users.component';
|
||||
import { CreateUserComponent } from '@app/users/create-user/create-user.component';
|
||||
@ -54,6 +55,7 @@ import {
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HomeComponent,
|
||||
ProfileComponent,
|
||||
AboutComponent,
|
||||
TenantsComponent,
|
||||
CreateTenantComponent,
|
||||
|
@ -11,6 +11,7 @@ export class SideBarNavComponent extends AppComponentBase {
|
||||
|
||||
menuItems: MenuItem[] = [
|
||||
new MenuItem(this.l("Strona domowa"), "", "home", "/app/home"),
|
||||
new MenuItem(this.l("Profil"), "", "person", "/app/profile"),
|
||||
|
||||
new MenuItem(this.l("Konkursy"), "", "list", "/app/categories-list"),
|
||||
new MenuItem(this.l("Dodaj konkurs"), "Pages.Create.Competition", "add", "/app/competition-create"),
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="image">
|
||||
<img src="assets/images/user.png" width="48" height="48" alt="User" />
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<!-- <div class="info-container">
|
||||
<div class="name" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{shownLoginName}}</div>
|
||||
<div class="email">{{appSession.user.emailAddress}}</div>
|
||||
<div class="btn-group user-helper-dropdown">
|
||||
@ -11,5 +11,5 @@
|
||||
<li><a (click)="logout()"><i class="material-icons">input</i>{{l('Wyloguj')}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
@ -20,7 +20,8 @@
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="javascript:void(0);" class="js-search" data-close="true"><i class="material-icons">search</i></a></li>
|
||||
<!-- <topbar-languageswitch></topbar-languageswitch> -->
|
||||
<li class="pull-right"><a href="javascript:void(0);" class="js-right-sidebar" data-close="true"><i class="material-icons">more_vert</i></a></li>
|
||||
<li><a href="javascript:void(0);" class="js-right-sidebar" data-close="true"><i class="material-icons">more_vert</i></a></li>
|
||||
<li><a (click)="logout()"><i class="material-icons">input</i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Component, Injector, ViewEncapsulation } from '@angular/core';
|
||||
import { AppComponentBase } from '@shared/app-component-base';
|
||||
import { AppAuthService } from '@shared/auth/app-auth.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './topbar.component.html',
|
||||
@ -11,8 +12,13 @@ export class TopBarComponent extends AppComponentBase {
|
||||
public logoUrl: string = '/assets/images/logo.png';
|
||||
|
||||
constructor(
|
||||
injector: Injector
|
||||
injector: Injector,
|
||||
private _authService: AppAuthService
|
||||
) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this._authService.logout();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
h2 {
|
||||
color: #771111;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<div id="profile-area">
|
||||
<div class="card">
|
||||
<div #cardBody class="body">
|
||||
<div>
|
||||
<h2 class="text-center font-weight-normal">MÓJ PROFIL</h2>
|
||||
</div>
|
||||
|
||||
<form #profileForm="ngForm" method="post" novalidate (ngSubmit)="saveProfile()">
|
||||
|
||||
<div class="form-group form-float">
|
||||
<div class="form-line focused">
|
||||
<input materialInput class="form-control" type="text" [(ngModel)]="profile.name" name="Name" required maxlength="32" />
|
||||
<label class="form-label">{{l('Imię')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-float">
|
||||
<div class="form-line focused">
|
||||
<input materialInput class="form-control" type="text" [(ngModel)]="profile.surname" name="Surname" required maxlength="32" />
|
||||
<label class="form-label">{{l('Nazwisko')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-float">
|
||||
<div class="form-line focused">
|
||||
<input materialInput class="form-control" type="email" [(ngModel)]="profile.emailAddress" name="EmailAddress" required maxlength="255" pattern="^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$" />
|
||||
<label class="form-label">{{l('Adres e-mail')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-float">
|
||||
<div class="form-line focused">
|
||||
<input materialInput class="form-control" type="text" autocomplete="off" [(ngModel)]="profile.userName" name="UserName" required maxlength="32" />
|
||||
<label class="form-label">{{l('Login')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="profile.participantClass !== 0" class="form-group form-float">
|
||||
<div class="form-line">
|
||||
<select class="form-control"
|
||||
[(ngModel)]="profile.participantClass"
|
||||
name="ParticipantClass"
|
||||
required>
|
||||
<option *ngFor="let possibleClass of possibleClasses" [value]="possibleClass.value">
|
||||
{{possibleClass.viewValue}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-success" [disabled]="!profileForm.form.valid">{{l("Zapisz profil")}}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,70 @@
|
||||
import { Component, Injector, ElementRef, OnInit, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserProfileServiceProxy, UserProfileDto } from '@shared/service-proxies/service-proxies'
|
||||
import { AppComponentBase } from '@shared/app-component-base';
|
||||
import { accountModuleAnimation } from '@shared/animations/routerTransition';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { Subscription } from 'rxjs/Rx';
|
||||
|
||||
@Component({
|
||||
templateUrl: './profile.component.html',
|
||||
styleUrls: ['./profile.component.css'],
|
||||
animations: [accountModuleAnimation()]
|
||||
})
|
||||
export class ProfileComponent extends AppComponentBase implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
@ViewChild('cardBody') cardBody: ElementRef;
|
||||
|
||||
public profileAreaId: string = 'profile-area';
|
||||
|
||||
public profile: UserProfileDto = new UserProfileDto();
|
||||
|
||||
private profileSubscription: Subscription;
|
||||
|
||||
public possibleClasses = [
|
||||
{value: 1, viewValue: 'Klasa 1'},
|
||||
{value: 2, viewValue: 'Klasa 2'},
|
||||
{value: 3, viewValue: 'Klasa 3'},
|
||||
{value: 4, viewValue: 'Klasa 4'},
|
||||
{value: 5, viewValue: 'Klasa 5'},
|
||||
{value: 6, viewValue: 'Klasa 6'},
|
||||
{value: 7, viewValue: 'Klasa 7'},
|
||||
{value: 8, viewValue: 'Klasa 8'}
|
||||
];
|
||||
|
||||
constructor(
|
||||
injector: Injector,
|
||||
private userProfileService: UserProfileServiceProxy,
|
||||
private router: Router,
|
||||
) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.setBusy(this.profileAreaId);
|
||||
|
||||
this.profileSubscription = this.userProfileService.getProfile()
|
||||
.pipe(finalize(() => { this.clearBusy(this.profileAreaId); }))
|
||||
.subscribe((result: UserProfileDto) => {
|
||||
this.profile = result;
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
if (this.profileSubscription) {
|
||||
this.profileSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public saveProfile(): void {
|
||||
this.profile.participantClass = +this.profile.participantClass;
|
||||
this.userProfileService.updateProfile(this.profile)
|
||||
.pipe(finalize(() => { this.router.navigate(['app/home']); }))
|
||||
.subscribe(() => {
|
||||
this.notify.success(this.l('Zapisano profil'));
|
||||
});
|
||||
}
|
||||
}
|
@ -2065,6 +2065,122 @@ export class UserServiceProxy {
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserProfileServiceProxy {
|
||||
private http: HttpClient;
|
||||
private baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
|
||||
this.http = http;
|
||||
this.baseUrl = baseUrl ? baseUrl : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Success
|
||||
*/
|
||||
getProfile(): Observable<UserProfileDto> {
|
||||
let url_ = this.baseUrl + "/api/services/app/UserProfile/GetProfile";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ : any = {
|
||||
observe: "response",
|
||||
responseType: "blob",
|
||||
headers: new HttpHeaders({
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
})
|
||||
};
|
||||
|
||||
return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
|
||||
return this.processGetProfile(response_);
|
||||
})).pipe(_observableCatch((response_: any) => {
|
||||
if (response_ instanceof HttpResponseBase) {
|
||||
try {
|
||||
return this.processGetProfile(<any>response_);
|
||||
} catch (e) {
|
||||
return <Observable<UserProfileDto>><any>_observableThrow(e);
|
||||
}
|
||||
} else
|
||||
return <Observable<UserProfileDto>><any>_observableThrow(response_);
|
||||
}));
|
||||
}
|
||||
|
||||
protected processGetProfile(response: HttpResponseBase): Observable<UserProfileDto> {
|
||||
const status = response.status;
|
||||
const responseBlob =
|
||||
response instanceof HttpResponse ? response.body :
|
||||
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
|
||||
|
||||
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
|
||||
if (status === 200) {
|
||||
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||
result200 = resultData200 ? UserProfileDto.fromJS(resultData200) : new UserProfileDto();
|
||||
return _observableOf(result200);
|
||||
}));
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
}));
|
||||
}
|
||||
return _observableOf<UserProfileDto>(<any>null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @userProfile (optional)
|
||||
* @return Success
|
||||
*/
|
||||
updateProfile(userProfile: UserProfileDto | null | undefined): Observable<void> {
|
||||
let url_ = this.baseUrl + "/api/services/app/UserProfile/UpdateProfile";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(userProfile);
|
||||
|
||||
let options_ : any = {
|
||||
body: content_,
|
||||
observe: "response",
|
||||
responseType: "blob",
|
||||
headers: new HttpHeaders({
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
};
|
||||
|
||||
return this.http.request("put", url_, options_).pipe(_observableMergeMap((response_ : any) => {
|
||||
return this.processUpdateProfile(response_);
|
||||
})).pipe(_observableCatch((response_: any) => {
|
||||
if (response_ instanceof HttpResponseBase) {
|
||||
try {
|
||||
return this.processUpdateProfile(<any>response_);
|
||||
} catch (e) {
|
||||
return <Observable<void>><any>_observableThrow(e);
|
||||
}
|
||||
} else
|
||||
return <Observable<void>><any>_observableThrow(response_);
|
||||
}));
|
||||
}
|
||||
|
||||
protected processUpdateProfile(response: HttpResponseBase): Observable<void> {
|
||||
const status = response.status;
|
||||
const responseBlob =
|
||||
response instanceof HttpResponse ? response.body :
|
||||
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
|
||||
|
||||
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
|
||||
if (status === 200) {
|
||||
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
|
||||
return _observableOf<void>(<any>null);
|
||||
}));
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
}));
|
||||
}
|
||||
return _observableOf<void>(<any>null);
|
||||
}
|
||||
}
|
||||
|
||||
export class IsTenantAvailableInput implements IIsTenantAvailableInput {
|
||||
tenancyName: string;
|
||||
|
||||
@ -4389,6 +4505,69 @@ export interface IPagedResultDtoOfUserDto {
|
||||
items: UserDto[] | undefined;
|
||||
}
|
||||
|
||||
export class UserProfileDto implements IUserProfileDto {
|
||||
name: string;
|
||||
surname: string;
|
||||
userName: string;
|
||||
emailAddress: string;
|
||||
participantClass: number;
|
||||
id: number | undefined;
|
||||
|
||||
constructor(data?: IUserProfileDto) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
(<any>this)[property] = (<any>data)[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(data?: any) {
|
||||
if (data) {
|
||||
this.name = data["name"];
|
||||
this.surname = data["surname"];
|
||||
this.userName = data["userName"];
|
||||
this.emailAddress = data["emailAddress"];
|
||||
this.participantClass = data["participantClass"];
|
||||
this.id = data["id"];
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): UserProfileDto {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
let result = new UserProfileDto();
|
||||
result.init(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
toJSON(data?: any) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["name"] = this.name;
|
||||
data["surname"] = this.surname;
|
||||
data["userName"] = this.userName;
|
||||
data["emailAddress"] = this.emailAddress;
|
||||
data["participantClass"] = this.participantClass;
|
||||
data["id"] = this.id;
|
||||
return data;
|
||||
}
|
||||
|
||||
clone(): UserProfileDto {
|
||||
const json = this.toJSON();
|
||||
let result = new UserProfileDto();
|
||||
result.init(json);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IUserProfileDto {
|
||||
name: string;
|
||||
surname: string;
|
||||
userName: string;
|
||||
emailAddress: string;
|
||||
participantClass: number;
|
||||
id: number | undefined;
|
||||
}
|
||||
|
||||
export enum IsTenantAvailableOutputState {
|
||||
_1 = 1,
|
||||
_2 = 2,
|
||||
|
@ -16,6 +16,7 @@ import * as ApiServiceProxies from './service-proxies';
|
||||
ApiServiceProxies.CategoryServiceProxy,
|
||||
ApiServiceProxies.CompetitionCategoryServiceProxy,
|
||||
ApiServiceProxies.CompetitionServiceProxy,
|
||||
ApiServiceProxies.UserProfileServiceProxy,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AbpHttpInterceptor, multi: true }
|
||||
]
|
||||
})
|
||||
|
@ -0,0 +1,29 @@
|
||||
using Abp.Application.Services.Dto;
|
||||
using Abp.Authorization.Users;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SystemKonkursow.UserProfile.Dto
|
||||
{
|
||||
public class UserProfileDto : EntityDto<long>
|
||||
{
|
||||
[Required]
|
||||
[StringLength(AbpUserBase.MaxNameLength)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(AbpUserBase.MaxSurnameLength)]
|
||||
public string Surname { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(AbpUserBase.MaxUserNameLength)]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[StringLength(AbpUserBase.MaxEmailAddressLength)]
|
||||
public string EmailAddress { get; set; }
|
||||
|
||||
[Required]
|
||||
public int ParticipantClass { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using AutoMapper;
|
||||
using SystemKonkursow.Authorization.Users;
|
||||
|
||||
namespace SystemKonkursow.UserProfile.Dto
|
||||
{
|
||||
public class UserProfileMapProfile : Profile
|
||||
{
|
||||
public UserProfileMapProfile()
|
||||
{
|
||||
CreateMap<User, UserProfileDto>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
|
||||
.ForMember(dest => dest.Surname, opt => opt.MapFrom(src => src.Surname))
|
||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.UserName))
|
||||
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress));
|
||||
|
||||
CreateMap<UserProfileDto, User>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
|
||||
.ForMember(dest => dest.Surname, opt => opt.MapFrom(src => src.Surname))
|
||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.UserName))
|
||||
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
using Abp.Authorization;
|
||||
using Abp.Domain.Repositories;
|
||||
using Abp.Domain.Uow;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Threading.Tasks;
|
||||
using SystemKonkursow.Authorization.Users;
|
||||
using SystemKonkursow.UserProfile.Dto;
|
||||
|
||||
namespace SystemKonkursow.UserProfile
|
||||
{
|
||||
public class UserProfileAppService : SystemKonkursowAppServiceBase
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly IRepository<Domain.Participant, int> _participantRepository;
|
||||
|
||||
public UserProfileAppService(UserManager userManager,
|
||||
IRepository<Domain.Participant, int> participantRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_participantRepository = participantRepository;
|
||||
}
|
||||
|
||||
[AbpAuthorize]
|
||||
public async Task<UserProfileDto> GetProfile()
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
var mappedProfile = ObjectMapper.Map<UserProfileDto>(user);
|
||||
|
||||
var isParticipant = await _userManager.IsParticipantUserAsync(user);
|
||||
|
||||
if (isParticipant)
|
||||
{
|
||||
var participant = await _participantRepository.GetAll()
|
||||
.FirstOrDefaultAsync(t => t.UserId == user.Id);
|
||||
|
||||
mappedProfile.ParticipantClass = participant.ParticipantClass;
|
||||
}
|
||||
else
|
||||
{
|
||||
mappedProfile.ParticipantClass = 0;
|
||||
}
|
||||
|
||||
return mappedProfile;
|
||||
}
|
||||
|
||||
[AbpAuthorize]
|
||||
[UnitOfWork]
|
||||
public async Task UpdateProfile(UserProfileDto userProfile)
|
||||
{
|
||||
var user = await _userManager.GetUserByIdAsync(userProfile.Id);
|
||||
|
||||
ObjectMapper.Map(userProfile, user);
|
||||
user.SetNormalizedNames();
|
||||
|
||||
CheckErrors(await _userManager.UpdateAsync(user));
|
||||
|
||||
if (userProfile.ParticipantClass != 0)
|
||||
{
|
||||
var participant = await _participantRepository.GetAll().FirstOrDefaultAsync(t => t.UserId == user.Id);
|
||||
|
||||
participant.ParticipantClass = userProfile.ParticipantClass;
|
||||
|
||||
await _participantRepository.UpdateAsync(participant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user