Closed davidcr01 closed 1 year ago
This feature will be contained in the Settings
part of the application. This new code has been added to the tab4
page:
HTML:
<ion-header>
<ion-toolbar>
<ion-title>
Settings
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="flex-center">
<ion-card>
<ion-card-content>
<ion-list>
<ion-item class="option" (click)="editPersonalInfo()">
<ion-label>Edit personal information</ion-label>
<ion-button slot="end" fill="clear">
<ion-icon class="icon" name="create"></ion-icon>
</ion-button>
</ion-item>
<ion-item class="logout" (click)="logout()">
<ion-label class="option">Logout</ion-label>
<ion-button slot="end" fill="clear">
<ion-icon class="icon" name="log-out"></ion-icon>
</ion-button>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
</div>
</ion-content>
Just as simple as a card with the available options. Notice that the logout option is added here.
TS:
@Component({
selector: 'app-tab4',
templateUrl: 'tab4.page.html',
styleUrls: ['tab4.page.scss']
})
export class Tab4Page {
constructor(private storageService: StorageService, private router: Router) {}
logout() {
this.storageService.destroyAll();
this.router.navigateByUrl('/login');
}
editPersonalInfo() {
this.router.navigate(['/edit-user']);
}
}
And the TS logic just redirects the user depending on the option. If the logout option is clicked, the Storage information is removed (and the token too) and the user is redirected to the main page.
To implement this feature, a new serializer and view have been added. The used model is the CustomUser
model.
The serializer:
class UserInfoPartialSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['email', 'first_name', 'last_name']
We just specify the fields that we are interested int.
The view:
class UserInfoAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
serializer = UserInfoPartialSerializer(user)
return Response(serializer.data)
def patch(self, request):
user = request.user
serializer = UserInfoPartialSerializer(user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
It returns or modifies the information given by the user identified by the token.
Finally, we add the path to the urls.py
file as api/users-info
, using the defined view.
If we test the new URL, we obtain the following results:
The PATCH request:
A new view has been defined in order to not to modify the CustomUser serializer and view, and to only allow the GET and PATCH options.
A new page has been created: edit-user
page.
The UI of this page consist in a form contained in a center card:
<form [formGroup]="userInfoForm" (ngSubmit)="saveUserInfo()">
<ion-list>
<ion-item>
<ion-label class="field" position="floating">Email</ion-label>
<ion-input type="email" formControlName="email"></ion-input>
</ion-item>
<ion-item>
<ion-label class="field" position="floating">First Name</ion-label>
<ion-input type="text" formControlName="firstName"></ion-input>
</ion-item>
<ion-item>
<ion-label class="field" position="floating">Last Name</ion-label>
<ion-input type="text" formControlName="lastName"></ion-input>
</ion-item>
</ion-list>
<div class="error-message" *ngIf="userInfoForm.get('email').invalid && userInfoForm.get('email').dirty">
Please enter a valid email address.
</div>
<div class="error-message" *ngIf="userInfoForm.get('firstName').invalid && userInfoForm.get('firstName').dirty">
Please enter a first name.
</div>
<div class="error-message" *ngIf="userInfoForm.get('lastName').invalid && userInfoForm.get('lastName').dirty">
Please enter a last name.
</div>
<ion-button type="submit" expand="full" [disabled]="userInfoForm.invalid">Save</ion-button>
</form>
Notice that we are using a ReactiveForm and div have been added to show error messages in case of the form is not valid. The submit button will be available if the form is valid.
About the TS logic:
export class EditUserPage implements OnInit {
userInfo: any = {};
userInfoForm: FormGroup;
constructor(private apiService: ApiService,
private storageService: StorageService,
private router: Router,
private toastService: ToastService,
private formBuilder: FormBuilder) {
this.userInfoForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
firstName: ['', [Validators.required, Validators.maxLength(20)]],
lastName: ['', [Validators.required, Validators.maxLength(20)]],
});
}
ngOnInit() {
this.getUserInfo();
}
async getUserInfo() {
(await this.apiService.getUserInfo()).subscribe((response: any) => {
this.userInfo = response;
this.userInfoForm.patchValue({
email: this.userInfo.email,
firstName: this.userInfo.first_name,
lastName: this.userInfo.last_name,
});
});
}
async saveUserInfo() {
if (this.userInfoForm.valid) {
const email = this.userInfoForm.get('email').value;
const firstName = this.userInfoForm.get('firstName').value;
const lastName = this.userInfoForm.get('lastName').value;
const body = {
'email': email,
'first_name': firstName,
'last_name': lastName
};
(await this.apiService.updateUserInfo(body)).subscribe(
() => {
this.toastService.showToast('Information updated succesfully!', 2000, 'top');
this.router.navigate(['/tabs/settings']);
},
(error) => {
this.toastService.showToast('An error was generated', 2000, 'top');
console.error('Error saving the information:', error);
}
);
} else {
console.error('Formulario inválido');
}
}
}
We define the getUserInfo
method to get the User information from the backend. Notice how the form is automatically fielded due to the pathValue
method of the form. This method uses a new ApiService method:
async getUserInfo(): Promise<Observable<any>> {
let url = `${this.baseURL}/api/users-info/`;
const accessToken = this.storageService.getAccessToken();
if (!accessToken) {
return throwError('Access token not found');
}
const decryptedToken = this.encryptionService.decryptData(await accessToken);
const headers = new HttpHeaders({
Authorization: `Token ${decryptedToken}`,
'Content-Type': 'application/json'
});
return this.http.get(url, { headers });
}
saveUserInfo
method, which gets the form fields and uses a new ApiService method to update the information. After that, the user is notified with a toast with the result and redirected to the setting page. The API method is:
async updateUserInfo(userInfo: any): Promise<Observable<any>> {
const url = `${this.baseURL}/api/users-info/`;
const accessToken = this.storageService.getAccessToken();
if (!accessToken) {
return throwError('Access token not found');
}
const decryptedToken = this.encryptionService.decryptData(await accessToken);
const headers = new HttpHeaders({
Authorization: `Token ${decryptedToken}`,
'Content-Type': 'application/json'
});
return this.http.patch(url, userInfo, { headers });
}
Very similar to the previous one, but in this case we are using PATCH. As we are not replacing all the information but specific fields, is better to use the PATCH method instead of the PUT method.
The following video shows how the information is fetched and saved:
https://github.com/davidcr01/WordlePlus/assets/72193239/dfe41334-8ef7-4eda-89a6-f4c3206ff384
A new functionality has been added to upload the avatar. In the form, this code has been added:
<ion-card>
<ion-card-header>
<ion-card-title>Change Avatar</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item>
<ion-label class="field">Current Avatar</ion-label>
<ion-avatar slot="end">
<img [src]="avatarPreview" />
</ion-avatar>
</ion-item>
<input type="file" accept="image/*" #avatarInput (change)="readAndPreviewAvatar(avatarInput.files[0])"/>
<ion-button expand="full" (click)="uploadAvatar()">Upload New Avatar</ion-button>
</ion-card-content>
</ion-card>
To upload and save the avatar, these methods have been added:
async uploadAvatar() {
const file = this.avatarInput.nativeElement.files[0];
if (file) {
console.log(file);
const reader = new FileReader();
reader.onloadend = () => {
const avatarData = reader.result as string;
console.log(avatarData);
this.saveAvatar(avatarData);
};
reader.readAsDataURL(file);
}
}
async saveAvatar(avatarData: string) {
try {
await (await this.apiService.saveAvatarImage(avatarData)).toPromise();
this.storageService.setAvatarUrl(avatarData);
this.toastService.showToast('Avatar updated successfully!', 2000, 'top');
this.getUserInfo(); // Refresh user info to update avatar preview
this.router.navigate(['/tabs/main'], { queryParams: { refresh: 'true' } });
} catch (error) {
console.error('Error uploading avatar:', error);
}
}
The file is selected from the input, reader by the reader
variable and send to the saveAvatar function. This function post the avatar to the API and saves it in the StorageService.
The AvatarView has been modified. As the image is sent encoded in base64 and decoded in base64 when returning it, is unnecessary to implement this logic. Instead, the base64 code of the image is stored in the database.
class AvatarView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, user_id):
try:
user = get_object_or_404(CustomUser, id=user_id)
if request.user == user:
if user.avatar:
avatar_data = user.avatar.read()
return JsonResponse({'avatar': avatar_data.decode('utf-8')}, status=200, safe=False)
else:
return Response({'detail': 'Avatar not available.'}, status=404)
else:
return Response({'detail': 'You do not have permission to get the avatar.'}, status=403)
except CustomUser.DoesNotExist:
return Response({'detail': 'The specified user does not exist.'}, status=404)
def post(self, request, user_id):
try:
user = get_object_or_404(CustomUser, id=user_id)
if request.user == user:
avatar_data = request.data.get('avatar')
if avatar_data:
# Delete the existing avatar if it exists
if user.avatar:
user.avatar.delete()
# Save the avatar image without encoding or decoding
filename = f'{user_id}_avatar.png'
user.avatar.save(filename, ContentFile(avatar_data.encode('utf-8')))
return Response({'detail': 'Avatar uploaded correctly.'}, status=200)
else:
return Response({'detail': 'No avatar image attached.'}, status=400)
else:
return Response({'detail': 'You do not have permission to upload an avatar.'}, status=403)
except CustomUser.DoesNotExist:
return Response({'detail': 'The specified user does not exist.'}, status=404)
The result is:
https://github.com/davidcr01/WordlePlus/assets/72193239/69e4f033-820f-43ad-b329-339846712f2a
Description
As a player, is necessary to implement a new view and logic to allow the user to edit the personal information.
Tasks