Changeset 2a9d9d1 for src


Ignore:
Timestamp:
11/03/21 16:42:35 (3 years ago)
Author:
Стојков Марко <mst@…>
Branches:
dev
Children:
b9d7ae5
Parents:
80e2fe0
Message:

Set correct answer for question by question owner

Location:
src
Files:
23 added
24 edited

Legend:

Unmodified
Added
Removed
  • src/Clients/Angular/finki-chattery/src/app/core/services/auth.service.ts

    r80e2fe0 r2a9d9d1  
    2424  public user: ApplicationUser | null = null;
    2525  public oidcUser: User | null = null;
     26  public selfUser: SelfUserResponse | null = null;
    2627
    2728  constructor(private baseApi: BaseApiService) {
     
    3536        user?.profile.isVerified
    3637      );
     38
     39      this.selfUserDto().subscribe((selfUser) => {
     40        if (selfUser) {
     41          this.selfUser = selfUser;
     42        }
     43      });
    3744    });
    3845  }
     
    8693        user.profile.isVerified
    8794      );
     95
     96      if (!this.selfUser) {
     97        this.selfUserDto().subscribe((selfUser) => {
     98          if (selfUser) {
     99            this.selfUser = selfUser;
     100          }
     101        });
     102      }
    88103    });
    89104  }
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-facade.service.ts

    r80e2fe0 r2a9d9d1  
    33import { Action, Store } from '@ngrx/store';
    44import { Observable, Subject } from 'rxjs';
    5 import { filter } from 'rxjs/operators';
     5import { filter, map } from 'rxjs/operators';
    66
    77import {
     
    1212  VoteType
    1313} from 'src/app/shared-app/models';
     14import { AuthService } from '../services';
    1415import {
    1516  EffectStartedWorking,
     
    1819  GetQuestionState,
    1920  GetSearchQuestions,
     21  SetCorrectAnswer,
    2022  VoteAnswer
    2123} from './question-state/question.actions';
     
    3133  effectWorking$: Observable<boolean | HttpErrorResponse>;
    3234
    33   constructor(private store: Store<QuestionState>) {
     35  constructor(private store: Store<QuestionState>, private auth: AuthService) {
    3436    this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(filter((effect) => effect !== null));
     37  }
     38
     39  public currentQuestionOwnedByCurrentUser(): Observable<boolean> {
     40    return this.getQuestion().pipe(map((question) => this.auth.selfUser?.student?.uid === question.student.uid));
    3541  }
    3642
     
    7177  }
    7278
     79  public setCorrectAnswer(questionUid: string, answerUid: string): void {
     80    this.dispatchEffect(new SetCorrectAnswer(questionUid, answerUid));
     81  }
     82
    7383  public voteAnswer(answerUid: string, questionUid: string, voteType: VoteType): void {
    7484    this.dispatchEffect(new VoteAnswer(questionUid, answerUid, voteType));
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.actions.ts

    r80e2fe0 r2a9d9d1  
    1313  GetQuestionState = '[Question] Get state',
    1414  GetQuestionStateSuccess = '[Question] Get state success',
     15  SetCorrectAnswer = '[Question] Set Correct Answer',
     16  SetCorrectAnswerSuccess = '[Question] Set Correct Answer success',
    1517  GetPreviewQuestionsLatest = '[Question] Get preview questions Latest',
    1618  GetPreviewQuestionsLatestSuccess = '[Question] Get preview questions Latest Success',
     
    3638
    3739  constructor(public payload: QuestionStateViewModel) {}
     40}
     41
     42export class SetCorrectAnswer implements Action {
     43  readonly type = QuestionActionTypes.SetCorrectAnswer;
     44
     45  constructor(public questionUid: string, public answerUid: string) {}
     46}
     47
     48export class SetCorrectAnswerSuccess implements Action {
     49  readonly type = QuestionActionTypes.SetCorrectAnswerSuccess;
     50
     51  constructor(public payload: string) {}
    3852}
    3953
     
    110124  | GetSearchQuestionsSuccess
    111125  | VoteAnswerSuccess
     126  | SetCorrectAnswerSuccess
    112127  | EffectStartedWorking
    113128  | EffectFinishedWorking
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.effects.ts

    r80e2fe0 r2a9d9d1  
    11import { Injectable } from '@angular/core';
    22import { act, Actions, createEffect, ofType } from '@ngrx/effects';
    3 import { catchError, filter, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
     3import { catchError, filter, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
    44import { PreviewQuestionsOrderEnum, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
    55import { TranslateFromJsonService } from 'src/app/shared-app/services';
    66
    77import { BaseApiService } from 'src/app/shared-app/services/base-api.service';
     8import { NotificationService } from '../../services/notification.service';
    89import { QuestionFacadeService } from '../question-facade.service';
    910import { VoteAnswerRequest } from './question-state-request.models';
     
    2122  GetSearchQuestionsSuccess,
    2223  QuestionActionTypes,
     24  SetCorrectAnswer,
     25  SetCorrectAnswerSuccess,
    2326  VoteAnswer,
    2427  VoteAnswerSuccess
     
    3437    private api: BaseApiService,
    3538    private translate: TranslateFromJsonService,
    36     private facade: QuestionFacadeService
     39    private facade: QuestionFacadeService,
     40    private notification: NotificationService
    3741  ) {}
    3842
     
    113117        const body = new VoteAnswerRequest(action.voteType);
    114118        return this.api.post<VoteAnswerResponse>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/votes`, body).pipe(
     119          tap((state) => this.notification.successNotification('sucess-vote')),
    115120          switchMap((state) => [new VoteAnswerSuccess(QuestionMapper.ToVoteAnswerViewModel(state)), new EffectFinishedWorking()]),
    116121          catchError((err) => [new EffectFinishedWorkingError(err)])
     
    119124    );
    120125  });
     126
     127  setCorrectAnswer$ = createEffect(() => {
     128    return this.actions$.pipe(
     129      ofType<SetCorrectAnswer>(QuestionActionTypes.SetCorrectAnswer),
     130      mergeMap((action) => {
     131        return this.api.put<string>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/correct`).pipe(
     132          tap((state) => this.notification.successNotification('success-correct-answer')),
     133          switchMap((state) => [new SetCorrectAnswerSuccess(state), new EffectFinishedWorking()]),
     134          catchError((err) => [new EffectFinishedWorkingError(err)])
     135        );
     136      })
     137    );
     138  });
    121139}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.reducers.ts

    r80e2fe0 r2a9d9d1  
    1 import { VoteType } from 'src/app/shared-app/models';
     1import { AnswerQuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    22import { QuestionAction, QuestionActionTypes } from './question.actions';
    33import { initialState, QuestionState } from './question.state';
     
    6161      };
    6262    }
     63    case QuestionActionTypes.SetCorrectAnswerSuccess: {
     64      if (state.question) {
     65        return {
     66          ...state,
     67          question: {
     68            ...state.question,
     69            answers: state.question.answers.map((x) => {
     70              if (x.correctAnswer) {
     71                return {
     72                  ...x,
     73                  correctAnswer: false
     74                };
     75              }
     76              if (x.uid === action.payload) {
     77                return {
     78                  ...x,
     79                  correctAnswer: true
     80                };
     81              }
     82              return x;
     83            })
     84          }
     85        };
     86      }
     87
     88      return {
     89        ...state
     90      };
     91    }
    6392    case QuestionActionTypes.EffectStartedWorking: {
    6493      return {
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.html

    r80e2fe0 r2a9d9d1  
    1 <div>
     1<div class="main-div">
    22  <div class="vote-icons" fxLayout="column" fxLayoutAlign="center center">
    33    <mat-icon class="vote-icon" (click)="voted(VoteType.Upvote)" [inline]="true">arrow_drop_up</mat-icon>
     
    77      >check</mat-icon
    88    >
     9    <mat-icon
     10      *ngIf="canSetCorrectAnswer && !correct"
     11      [inline]="true"
     12      class="text-center text-bold green set-correct-answer"
     13      (click)="setCorrectAnswer.emit()"
     14      >check</mat-icon
     15    >
    916  </div>
    1017</div>
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.scss

    r80e2fe0 r2a9d9d1  
    1414  cursor: pointer;
    1515}
     16
     17.show-on-hover {
     18  display: none;
     19}
     20
     21.set-correct-answer {
     22  opacity: 0.3;
     23  visibility: hidden;
     24}
     25
     26.main-div:hover > .vote-icons > .set-correct-answer {
     27  visibility: visible;
     28}
     29
     30.main-div {
     31  height: 100%;
     32}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.ts

    r80e2fe0 r2a9d9d1  
    1010export class VoteComponent implements OnInit {
    1111  @Input() voteCount: number | undefined;
     12  @Input() canSetCorrectAnswer: boolean | null = false;
    1213  @Input() correct = false;
    1314  @Output() voteClicked = new EventEmitter<VoteType>();
     15  @Output() setCorrectAnswer = new EventEmitter<void>();
    1416
    1517  VoteType = VoteType;
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.html

    r80e2fe0 r2a9d9d1  
    1515        <div fxLayout="row wrap" fxLayoutAlign="space-around none">
    1616          <app-vote
     17            [canSetCorrectAnswer]="canSetCorrectAnswer | async"
    1718            [voteCount]="answer.votesCount"
    1819            [correct]="answer.correctAnswer"
    1920            (voteClicked)="answerVoted($event, answer.uid, question.uid)"
     21            (setCorrectAnswer)="setCorrectAnswer(question.uid, answer.uid)"
    2022            fxFlex="6%"
    2123          ></app-vote>
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.ts

    r80e2fe0 r2a9d9d1  
    1313})
    1414export class QuestionPreviewComponent implements OnInit {
     15  canSetCorrectAnswer = this.questionFacade.currentQuestionOwnedByCurrentUser();
    1516  question!: QuestionStateViewModel;
    1617  working = true;
     
    3435    this.questionFacade.voteAnswer(answerUid, questionUid, voteType);
    3536  }
     37
     38  setCorrectAnswer(questionUid: string, answerUid: string): void {
     39    this.questionFacade.setCorrectAnswer(questionUid, answerUid);
     40  }
    3641}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/user.models.ts

    r80e2fe0 r2a9d9d1  
    99}
    1010
    11 export class SelfUserResponse {
    12   uid!: string;
    13 }
    14 
    1511export enum ApplicationUserType {
    1612  Student = 'Student',
     
    1915  Guest = 'Guest'
    2016}
     17
     18export class SelfUserResponse {
     19  public student?: StudentSelfResponse | null;
     20  public teacher?: TeacherSelfResponse | null;
     21  public moderator?: ModeratorSelfResponse | null;
     22}
     23
     24export class StudentSelfResponse {
     25  public uid!: string;
     26  public applicationUserId!: number;
     27  public index!: string;
     28  public reputation!: number;
     29  public imageUrl!: string;
     30  public questions!: StudentQuestionResponse[];
     31  public teams!: StudentTeamResponse[];
     32}
     33
     34export class StudentQuestionResponse {
     35  public questionUid!: string;
     36  public title!: string;
     37}
     38
     39export class StudentTeamResponse {
     40  public teamUid!: string;
     41  public name!: string;
     42}
     43
     44export class ModeratorSelfResponse {
     45  public uid!: string;
     46  public applicationUserId!: number;
     47}
     48
     49export class TeacherSelfResponse {
     50  public uid!: string;
     51  public applicationUserId!: number;
     52  public teams!: TeacherTeamResponse[];
     53}
     54
     55export class TeacherTeamResponse {
     56  public teamUid!: string;
     57  public name!: string;
     58}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/services/base-api.service.ts

    r80e2fe0 r2a9d9d1  
    1919
    2020  public getSelfUser(): Observable<SelfUserResponse> {
    21     return this.get<SelfUserResponse>('self');
     21    return this.get<SelfUserResponse>('v1/self');
    2222  }
    2323
  • src/Clients/Angular/finki-chattery/src/assets/translations/en.json

    r80e2fe0 r2a9d9d1  
    5353  "ask-question-stepper-ask": "Ask question",
    5454  "ask-question-ask-button-back": "Edit question",
     55  "sucess-vote": "Successfully voted answer",
     56  "success-correct-answer": "Successfully set correct answer",
    5557  "ask-question-ask-button": "Ask question",
    5658  "AnswerAlreadyUpvoted": "You have already upvoted this answer",
  • src/FinkiChattery/FinkiChattery.Api/Controllers/v1/AnswersController.cs

    r80e2fe0 r2a9d9d1  
    3030            return Ok(answerUid);
    3131        }
     32
     33        [HttpPut("{answerUid:Guid}/correct")]
     34        [Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme, Policy = AuthenticationPolicy.Student)]
     35        public async Task<IActionResult> MarkAnswerCorrect([FromRoute] Guid questionUid, [FromRoute] Guid answerUid)
     36        {
     37            await MediatorService.SendAsync(new MarkAnswerCorrectCommand(questionUid, answerUid));
     38            return Ok(answerUid);
     39        }
    3240    }
    3341}
  • src/FinkiChattery/FinkiChattery.Commands/Questioning/QuestioningErrorCodes.cs

    r80e2fe0 r2a9d9d1  
    1313        public const string StudentHasBadReputation = "StudentHasBadReputation";
    1414        public const string AnswerTextLengthInvalid = "AnswerTextLengthInvalid";
    15         public const string QuestionNotFound = "QuestionNotFound";
     15        public const string QuestionNotFound = "QuestionNotFound";
     16        public const string AnswerInQuestionNotFound = "AnswerInQuestionNotFound";
     17        public const string StudentDoesNotOwnQuestion = "StudentDoesNotOwnQuestion";
     18        public const string AnswerIsAlreadyMarkedAsCorrect = "AnswerIsAlreadyMarkedAsCorrect";
    1619    }
    1720}
  • src/FinkiChattery/FinkiChattery.Persistence/Models/Moderator.cs

    r80e2fe0 r2a9d9d1  
    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using System.Text;
    5 using System.Threading.Tasks;
    6 
    7 namespace FinkiChattery.Persistence.Models
     1namespace FinkiChattery.Persistence.Models
    82{
    93    public class Moderator : BaseEntity
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Contracts/IAnswerRepo.cs

    r80e2fe0 r2a9d9d1  
    11using FinkiChattery.Persistence.Models;
     2using System;
     3using System.Threading.Tasks;
    24
    35namespace FinkiChattery.Persistence.Repositories
     
    57    public interface IAnswerRepo : IRepository<Answer>
    68    {
     9        Task<bool> AnswerInQuestionExists(Guid questionUid, Guid answerUid);
    710    }
    811}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Contracts/IQuestionRepo.cs

    r80e2fe0 r2a9d9d1  
    1616     
    1717        Task<List<QuestionPreviewDto>> GetPreviewQuestionsPopular();
     18
     19        Task<bool> QuestionIsOwnedByStudent(Guid questionUid, long applicationUserId);
     20
     21        Task<Question> GetQuestionWithAnswersAndStudents(Guid questionUid);
    1822    }
    1923}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Contracts/IStudentRepo.cs

    r80e2fe0 r2a9d9d1  
    11using FinkiChattery.Persistence.Models;
     2using FinkiChattery.Persistence.Repositories.Contracts;
    23using System.Threading.Tasks;
    34
     
    78    {
    89        public Task<Student> GetStudent(long applicationUserFk);
     10        public Task<StudentSelfDto> GetStudentSelfDto(long applicationUserFk);
    911    }
    1012}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Implementations/AnswerRepo.cs

    r80e2fe0 r2a9d9d1  
    11using FinkiChattery.Persistence.Context;
    22using FinkiChattery.Persistence.Models;
     3using Microsoft.EntityFrameworkCore;
     4using System;
     5using System.Linq;
     6using System.Threading.Tasks;
    37
    48namespace FinkiChattery.Persistence.Repositories
     
    913        {
    1014        }
     15
     16        public async Task<bool> AnswerInQuestionExists(Guid questionUid, Guid answerUid)
     17        {
     18            return await DbSet
     19                .AsNoTracking()
     20                .Where(x => x.Question.Uid == questionUid && x.Uid == answerUid)
     21                .AnyAsync();
     22        }
    1123    }
    1224}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Implementations/QuestionRepo.cs

    r80e2fe0 r2a9d9d1  
    144144                .ToListAsync();
    145145        }
     146
     147        public async Task<bool> QuestionIsOwnedByStudent(Guid questionUid, long applicationUserId)
     148        {
     149            return await DbSet
     150                .Where(x => x.Uid == questionUid && x.Student.ApplicationUserFk == applicationUserId)
     151                .AnyAsync();
     152        }
     153
     154        public async Task<Question> GetQuestionWithAnswersAndStudents(Guid questionUid)
     155        {
     156            return await DbSet
     157                .Include(x => x.Answers).ThenInclude(x => x.Student)
     158                .FirstOrDefaultAsync(x => x.Uid == questionUid);
     159        }
    146160    }
    147161}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Implementations/StudentRepo.cs

    r80e2fe0 r2a9d9d1  
    11using FinkiChattery.Persistence.Context;
    22using FinkiChattery.Persistence.Models;
     3using FinkiChattery.Persistence.Repositories.Contracts;
    34using Microsoft.EntityFrameworkCore;
     5using System.Linq;
    46using System.Threading.Tasks;
    57
     
    1618            return await DbSet.FirstOrDefaultAsync(x => x.ApplicationUserFk == applicationUserFk);
    1719        }
     20
     21        public async Task<StudentSelfDto> GetStudentSelfDto(long applicationUserFk)
     22        {
     23            return await DbSet
     24                .AsNoTracking()
     25                .Include(x => x.Questions)
     26                .Include(x => x.StudentTeams).ThenInclude(x => x.Team)
     27                .Where(x => x.ApplicationUserFk == applicationUserFk)
     28                .Select(x => new StudentSelfDto(x.Uid,
     29                                                x.ApplicationUserFk,
     30                                                x.IndexNumber,
     31                                                x.Reputation,
     32                                                x.ImageUrl,
     33                                                x.Questions.Select(y => new StudentQuestionDto(y.Uid, y.Title)),
     34                                                x.StudentTeams.Select(y => new StudentTeamDto(y.Team.Uid, y.Team.Name))))
     35                .FirstOrDefaultAsync();
     36        }
    1837    }
    1938}
  • src/FinkiChattery/FinkiChattery.Persistence/UnitOfWork/Contracts/IUnitOfWork.cs

    r80e2fe0 r2a9d9d1  
    1313        IVoteRepo Votes { get; }
    1414        IAnswerRepo Answers { get; }
     15        ITeacherRepo Teachers { get; }
     16        IModeratorRepo Moderators { get; }
    1517        Task<int> SaveAsync();
    1618    }
  • src/FinkiChattery/FinkiChattery.Persistence/UnitOfWork/Implementations/UnitOfWork.cs

    r80e2fe0 r2a9d9d1  
    1414        private VoteRepo _votes;
    1515        private AnswerRepo _answers;
     16        private ModeratorRepo _moderators;
     17        private TeacherRepo _teachers;
    1618
    1719        public UnitOfWork(ApplicationDbContext dbContext)
    1820        {
    1921            DbContext = dbContext;
     22        }
     23
     24        public IModeratorRepo Moderators
     25        {
     26            get
     27            {
     28                if (_moderators == null)
     29                {
     30                    _moderators = new ModeratorRepo(DbContext);
     31                }
     32
     33                return _moderators;
     34            }
     35        }
     36
     37        public ITeacherRepo Teachers
     38        {
     39            get
     40            {
     41                if (_teachers == null)
     42                {
     43                    _teachers = new TeacherRepo(DbContext);
     44                }
     45
     46                return _teachers;
     47            }
    2048        }
    2149
Note: See TracChangeset for help on using the changeset viewer.