Changeset 6901f8b


Ignore:
Timestamp:
11/04/21 17:01:30 (3 years ago)
Author:
Стојков Марко <mst@…>
Branches:
dev
Children:
caaf82d
Parents:
1e0d869 (diff), b9d7ae5 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merged dev

Location:
src
Files:
50 added
2 deleted
37 edited
2 moved

Legend:

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

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    22import { Injectable } from '@angular/core';
    33import { Action, Store } from '@ngrx/store';
    4 import { Observable, Subject, throwError } from 'rxjs';
    5 import { catchError, filter, map } from 'rxjs/operators';
     4import { Observable, Subject } from 'rxjs';
     5import { filter, map } from 'rxjs/operators';
    66
    77import {
     
    99  PreviewQuestionViewModel,
    1010  QuestionStateViewModel,
    11   SearchQuestionsQueryViewModel
     11  SearchQuestionsQueryViewModel,
     12  VoteType
    1213} from 'src/app/shared-app/models';
     14import { AuthService } from '../services';
    1315import {
    1416  EffectStartedWorking,
     
    1618  GetPreviewQuestionsPopular,
    1719  GetQuestionState,
    18   GetSearchQuestions
     20  GetSearchQuestions,
     21  SetCorrectAnswer,
     22  VoteAnswer
    1923} from './question-state/question.actions';
    2024import { questionStateQuery } from './question-state/question.selectors';
     
    2933  effectWorking$: Observable<boolean | HttpErrorResponse>;
    3034
    31   constructor(private store: Store<QuestionState>) {
    32     this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(
    33       filter((effect) => effect !== null),
    34       map((effect) => {
    35         if (effect instanceof HttpErrorResponse) {
    36           throw effect;
    37         } else {
    38           return effect;
    39         }
    40       }),
    41       catchError((err) => {
    42         return throwError(err);
    43       })
    44     );
     35  constructor(private store: Store<QuestionState>, private auth: AuthService) {
     36    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));
    4541  }
    4642
     
    8177  }
    8278
     79  public setCorrectAnswer(questionUid: string, answerUid: string): void {
     80    this.dispatchEffect(new SetCorrectAnswer(questionUid, answerUid));
     81  }
     82
     83  public voteAnswer(answerUid: string, questionUid: string, voteType: VoteType): void {
     84    this.dispatchEffect(new VoteAnswer(questionUid, answerUid, voteType));
     85  }
     86
    8387  public fetchQuestion(questionUid: string): void {
    8488    this.dispatchEffect(new GetQuestionState(questionUid));
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question-state-response.models.ts

    r1e0d869 r6901f8b  
     1import { VoteType } from 'src/app/shared-app/models';
     2
    13export class QuestionStateResponse {
    24  public uid!: string;
     
    3436  public correctAnswer!: boolean;
    3537  public createdOn!: moment.Moment;
    36   public upvotesCount!: number;
     38  public votesCount!: number;
    3739  public studentResponse!: AnswerStudentQuestionStateResponse;
    3840  public answerResponsesResponse!: AnswerResponseQuestionStateResponse[];
     
    7375  public text!: string;
    7476}
     77
     78export class VoteAnswerResponse {
     79  public answerUid!: string;
     80  public voteUid!: string;
     81  public voteType!: VoteType;
     82}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.actions.ts

    r1e0d869 r6901f8b  
    22import { Action } from '@ngrx/store';
    33
    4 import { PreviewQuestionViewModel, QuestionStateViewModel, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
     4import {
     5  PreviewQuestionViewModel,
     6  QuestionStateViewModel,
     7  SearchQuestionsQueryViewModel,
     8  VoteAnswerViewModel,
     9  VoteType
     10} from 'src/app/shared-app/models';
    511
    612export enum QuestionActionTypes {
    713  GetQuestionState = '[Question] Get state',
    814  GetQuestionStateSuccess = '[Question] Get state success',
     15  SetCorrectAnswer = '[Question] Set Correct Answer',
     16  SetCorrectAnswerSuccess = '[Question] Set Correct Answer success',
    917  GetPreviewQuestionsLatest = '[Question] Get preview questions Latest',
    1018  GetPreviewQuestionsLatestSuccess = '[Question] Get preview questions Latest Success',
     
    1321  GetSearchQuestions = '[Question] Get search questions',
    1422  GetSearchQuestionsSuccess = '[Question] Get search questions Success',
     23  VoteAnswer = '[Question] Vote answer',
     24  VoteAnswerSuccess = '[Question] Vote answer Success',
    1525  EffectStartedWorking = '[Question] Effect Started Working',
    1626  EffectFinishedWorking = '[Question] Effect Finished Working',
     
    2838
    2939  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) {}
    3052}
    3153
     
    6688}
    6789
     90export class VoteAnswer implements Action {
     91  readonly type = QuestionActionTypes.VoteAnswer;
     92
     93  constructor(public questionUid: string, public answerUid: string, public voteType: VoteType) {}
     94}
     95
     96export class VoteAnswerSuccess implements Action {
     97  readonly type = QuestionActionTypes.VoteAnswerSuccess;
     98
     99  constructor(public payload: VoteAnswerViewModel) {}
     100}
     101
    68102export class EffectStartedWorking implements Action {
    69103  readonly type = QuestionActionTypes.EffectStartedWorking;
     
    89123  | GetPreviewQuestionsPopularSuccess
    90124  | GetSearchQuestionsSuccess
     125  | VoteAnswerSuccess
     126  | SetCorrectAnswerSuccess
    91127  | EffectStartedWorking
    92128  | EffectFinishedWorking
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.effects.ts

    r1e0d869 r6901f8b  
    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';
    9 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     10import { VoteAnswerRequest } from './question-state-request.models';
     11import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1012import {
    1113  EffectFinishedWorking,
     
    1921  GetSearchQuestions,
    2022  GetSearchQuestionsSuccess,
    21   QuestionActionTypes
     23  QuestionActionTypes,
     24  SetCorrectAnswer,
     25  SetCorrectAnswerSuccess,
     26  VoteAnswer,
     27  VoteAnswerSuccess
    2228} from './question.actions';
    2329import { QuestionMapper } from './question.mapper';
     
    3137    private api: BaseApiService,
    3238    private translate: TranslateFromJsonService,
    33     private facade: QuestionFacadeService
     39    private facade: QuestionFacadeService,
     40    private notification: NotificationService
    3441  ) {}
    3542
     
    103110    );
    104111  });
     112
     113  voteAnswer$ = createEffect(() => {
     114    return this.actions$.pipe(
     115      ofType<VoteAnswer>(QuestionActionTypes.VoteAnswer),
     116      mergeMap((action) => {
     117        const body = new VoteAnswerRequest(action.voteType);
     118        return this.api.post<VoteAnswerResponse>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/votes`, body).pipe(
     119          tap((state) => this.notification.successNotification('sucess-vote')),
     120          switchMap((state) => [new VoteAnswerSuccess(QuestionMapper.ToVoteAnswerViewModel(state)), new EffectFinishedWorking()]),
     121          catchError((err) => [new EffectFinishedWorkingError(err)])
     122        );
     123      })
     124    );
     125  });
     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  });
    105139}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.mapper.ts

    r1e0d869 r6901f8b  
    1010  QuestionStateViewModel,
    1111  StudentQuestionStateViewModel,
    12   TeamQuestionStateViewModel
     12  TeamQuestionStateViewModel,
     13  VoteAnswerViewModel
    1314} from 'src/app/shared-app/models';
    1415import { TranslateFromJsonService } from 'src/app/shared-app/services';
    15 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     16import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1617
    1718export class QuestionMapper {
     
    5152          x.correctAnswer,
    5253          moment(x.createdOn),
    53           x.upvotesCount,
     54          x.votesCount,
    5455          answerStudent,
    5556          answerResponses
     
    113114    return questions;
    114115  }
     116
     117  public static ToVoteAnswerViewModel(response: VoteAnswerResponse): VoteAnswerViewModel {
     118    return new VoteAnswerViewModel(response.answerUid, response.voteUid, response.voteType);
     119  }
    115120}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.reducers.ts

    r1e0d869 r6901f8b  
     1import { AnswerQuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    12import { QuestionAction, QuestionActionTypes } from './question.actions';
    23import { initialState, QuestionState } from './question.state';
     
    2526        searchQuestionsQuery: action.query
    2627      };
     28    case QuestionActionTypes.VoteAnswerSuccess: {
     29      if (state.question) {
     30        return {
     31          ...state,
     32          question: {
     33            ...state.question,
     34            answers: state.question.answers.map((x) => {
     35              if (x.uid === action.payload.answerUid) {
     36                let votesCountNew = x.votesCount;
     37
     38                switch (action.payload.voteType) {
     39                  case VoteType.Upvote:
     40                    votesCountNew++;
     41                    break;
     42                  case VoteType.Downvote:
     43                    votesCountNew--;
     44                    break;
     45                }
     46
     47                return {
     48                  ...x,
     49                  votesCount: votesCountNew
     50                };
     51              }
     52
     53              return x;
     54            })
     55          }
     56        };
     57      }
     58
     59      return {
     60        ...state
     61      };
     62    }
     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    }
    2792    case QuestionActionTypes.EffectStartedWorking: {
    2893      return {
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.html

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    11import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
     2import { VoteType } from 'src/app/shared-app/models';
    23import { ButtonType } from '../button/button.models';
    3 
    4 export enum VoteType {
    5   Upvote,
    6   Downvote
    7 }
    84
    95@Component({
     
    1410export class VoteComponent implements OnInit {
    1511  @Input() voteCount: number | undefined;
     12  @Input() canSetCorrectAnswer: boolean | null = false;
    1613  @Input() correct = false;
    1714  @Output() voteClicked = new EventEmitter<VoteType>();
     15  @Output() setCorrectAnswer = new EventEmitter<void>();
    1816
    1917  VoteType = VoteType;
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.html

    r1e0d869 r6901f8b  
    1414      <mat-card-content>
    1515        <div fxLayout="row wrap" fxLayoutAlign="space-around none">
    16           <app-vote [voteCount]="answer.upvotesCount" [correct]="answer.correctAnswer" fxFlex="6%"></app-vote>
     16          <app-vote
     17            [canSetCorrectAnswer]="canSetCorrectAnswer | async"
     18            [voteCount]="answer.votesCount"
     19            [correct]="answer.correctAnswer"
     20            (voteClicked)="answerVoted($event, answer.uid, question.uid)"
     21            (setCorrectAnswer)="setCorrectAnswer(question.uid, answer.uid)"
     22            fxFlex="6%"
     23          ></app-vote>
    1724          <div fxFlex="92%">
    1825            <app-text-editor
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.ts

    r1e0d869 r6901f8b  
     1import { HttpErrorResponse } from '@angular/common/http';
    12import { Component, OnInit } from '@angular/core';
     3import { NotificationService } from 'src/app/core/services/notification.service';
    24import { QuestionFacadeService } from 'src/app/core/state/question-facade.service';
    35
    4 import { QuestionStateViewModel } from 'src/app/shared-app/models';
     6import { QuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    57import { ButtonType } from '../../generic/button/button.models';
    68
     
    1113})
    1214export class QuestionPreviewComponent implements OnInit {
     15  canSetCorrectAnswer = this.questionFacade.currentQuestionOwnedByCurrentUser();
    1316  question!: QuestionStateViewModel;
    1417  working = true;
    1518  ButtonType = ButtonType;
    16   constructor(private questionFacade: QuestionFacadeService) {}
     19  constructor(private questionFacade: QuestionFacadeService, private notification: NotificationService) {}
    1720
    1821  ngOnInit(): void {
     
    2124      this.working = false;
    2225    });
     26
     27    this.questionFacade.effectWorking$.subscribe((effect) => {
     28      if (effect instanceof HttpErrorResponse) {
     29        this.notification.handleErrorsNotification(effect.error);
     30      }
     31    });
     32  }
     33
     34  answerVoted(voteType: VoteType, answerUid: string, questionUid: string): void {
     35    this.questionFacade.voteAnswer(answerUid, questionUid, voteType);
     36  }
     37
     38  setCorrectAnswer(questionUid: string, answerUid: string): void {
     39    this.questionFacade.setCorrectAnswer(questionUid, answerUid);
    2340  }
    2441}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-enums.models.ts

    r1e0d869 r6901f8b  
    33  Popular
    44}
     5
     6export enum VoteType {
     7  Upvote,
     8  Downvote
     9}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-view-models.models.ts

    r1e0d869 r6901f8b  
     1import { VoteType } from '.';
     2
    13export class QuestionStateViewModel {
    24  constructor(
     
    3234    public correctAnswer: boolean,
    3335    public createdOn: moment.Moment,
    34     public upvotesCount: number,
     36    public votesCount: number,
    3537    public student: AnswerStudentQuestionStateViewModel,
    3638    public answerResponses: AnswerResponseQuestionStateViewModel[]
     
    7375  constructor(public text: string) {}
    7476}
     77
     78export class VoteAnswerViewModel {
     79  constructor(public answerUid: string, public voteUid: string, public voteType: VoteType) {}
     80}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/user.models.ts

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    5353  "ask-question-stepper-ask": "Ask question",
    5454  "ask-question-ask-button-back": "Edit question",
    55   "ask-question-ask-button": "Ask question"
     55  "sucess-vote": "Successfully voted answer",
     56  "success-correct-answer": "Successfully set correct answer",
     57  "ask-question-ask-button": "Ask question",
     58  "AnswerAlreadyUpvoted": "You have already upvoted this answer",
     59  "AnswerAlreadyDownvoted": "You have already downvoted this answer",
     60  "StudentHasBadReputation": "You have bad reputation and can not vote"
    5661}
  • src/FinkiChattery/FinkiChattery.Api/ApplicationServices/Questioning/Mapper/QuestionMapper.cs

    r1e0d869 r6901f8b  
    5454                    var answerStudent = new AnswerStudentQuestionStateResponse(x.StudentDto.Id, x.StudentDto.Uid, x.StudentDto.Index, x.StudentDto.ImageUrl, x.StudentDto.Reputation);
    5555
    56                     return new AnswerQuestionStateResponse(x.Id, x.Uid, x.Text, x.CorrectAnswer, x.CreatedOn, x.UpvotesCount, answerStudent, answerResponses);
     56                    return new AnswerQuestionStateResponse(x.Id, x.Uid, x.Text, x.CorrectAnswer, x.CreatedOn, x.VotesCount, answerStudent, answerResponses);
    5757                });
    5858            }
  • src/FinkiChattery/FinkiChattery.Api/Services/RegisterServices.cs

    r1e0d869 r6901f8b  
    11using FinkiChattery.Api.ApplicationServices.Authentication;
     2using FinkiChattery.Api.ApplicationServices.Questioning.EventHandlers;
    23using FinkiChattery.Api.Services;
    34using FinkiChattery.Commands.Questioning;
     
    3132            services.AddScoped<IEventService, EventService>();
    3233            services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
    33             services.AddMediatR(typeof(AskQuestionCommand), typeof(GetQuestionStateQuery));
     34            services.AddMediatR(typeof(AskQuestionCommand), typeof(GetQuestionStateQuery), typeof(UpdateAnswerVotesEventHandler));
    3435        }
    3536
     
    5455            });
    5556            services.AddHangfireServer();
     57
     58            services.AddScoped<IBackgroundJobClient>(provider =>
     59            {
     60                return new BackgroundJobClient(JobStorage.Current);
     61            });
    5662        }
    5763
  • src/FinkiChattery/FinkiChattery.Commands/Questioning/QuestioningErrorCodes.cs

    r1e0d869 r6901f8b  
    99        public const string CategoriesDontExist = "CategoriesDontExist";
    1010        public const string TeamDontExist = "TeamDontExist";
     11        public const string AnswerAlreadyUpvoted = "AnswerAlreadyUpvoted";
     12        public const string AnswerAlreadyDownvoted = "AnswerAlreadyDownvoted";
     13        public const string StudentHasBadReputation = "StudentHasBadReputation";
     14        public const string AnswerTextLengthInvalid = "AnswerTextLengthInvalid";
     15        public const string QuestionNotFound = "QuestionNotFound";
     16        public const string AnswerInQuestionNotFound = "AnswerInQuestionNotFound";
     17        public const string StudentDoesNotOwnQuestion = "StudentDoesNotOwnQuestion";
     18        public const string AnswerIsAlreadyMarkedAsCorrect = "AnswerIsAlreadyMarkedAsCorrect";
    1119    }
    1220}
  • src/FinkiChattery/FinkiChattery.Commands/Questioning/Validators/CategoriesUidsExist.cs

    r1e0d869 r6901f8b  
    1 using FinkiChattery.Persistence.Repositories;
    2 using FinkiChattery.Persistence.UnitOfWork;
     1using FinkiChattery.Persistence.UnitOfWork;
    32using FluentValidation.Validators;
    43using System;
  • src/FinkiChattery/FinkiChattery.Commands/Questioning/Validators/QuestioningFluentValidationRules.cs

    r1e0d869 r6901f8b  
    1616        }
    1717
     18        public static IRuleBuilderOptions<T, string> AnswerTextValidate<T>(this IRuleBuilder<T, string> ruleBuilder)
     19        {
     20            return ruleBuilder.NotNull().WithMessage(QuestioningErrorCodes.CantBeNull).MaximumLength(4000).WithMessage(QuestioningErrorCodes.AnswerTextLengthInvalid);
     21        }
     22
    1823        public static IRuleBuilderOptions<T, IEnumerable<U>> ListNotNull<T, U>(this IRuleBuilder<T, IEnumerable<U>> ruleBuilder)
    1924        {
  • src/FinkiChattery/FinkiChattery.Contracts/Questioning/GetQuestionState/QuestionStateResponse.cs

    r1e0d869 r6901f8b  
    8989    public class AnswerQuestionStateResponse
    9090    {
    91         public AnswerQuestionStateResponse(long id, Guid uid, string text, bool correctAnswer, DateTime createdOn, long upvotesCount, AnswerStudentQuestionStateResponse studentResponse, IEnumerable<AnswerResponseQuestionStateResponse> answerResponsesResponse)
     91        public AnswerQuestionStateResponse(long id, Guid uid, string text, bool correctAnswer, DateTime createdOn, long votesCount, AnswerStudentQuestionStateResponse studentResponse, IEnumerable<AnswerResponseQuestionStateResponse> answerResponsesResponse)
    9292        {
    9393            Id = id;
     
    9696            CorrectAnswer = correctAnswer;
    9797            CreatedOn = createdOn;
    98             UpvotesCount = upvotesCount;
     98            VotesCount = votesCount;
    9999            StudentResponse = studentResponse;
    100100            AnswerResponsesResponse = answerResponsesResponse;
     
    107107        public bool CorrectAnswer { get; }
    108108        public DateTime CreatedOn { get; }
    109         public long UpvotesCount { get; }
     109        public long VotesCount { get; }
    110110        public AnswerStudentQuestionStateResponse StudentResponse { get; }
    111111        public IEnumerable<AnswerResponseQuestionStateResponse> AnswerResponsesResponse { get; }
  • src/FinkiChattery/FinkiChattery.Database/FinkiChattery.Database.sqlproj

    r1e0d869 r6901f8b  
    7575    <Folder Include="dbo\Tables\QuestionCategory" />
    7676    <Folder Include="Snapshots" />
     77    <Folder Include="dbo\Tables\Vote" />
    7778  </ItemGroup>
    7879  <ItemGroup>
     
    8182    <Build Include="dbo\Tables\StudentTeam.sql" />
    8283    <Build Include="dbo\Tables\TeacherTeam.sql" />
    83     <Build Include="dbo\Tables\Upvote.sql" />
    8484    <Build Include="dbo\Tables\User\AspNetRoleClaims.sql" />
    8585    <Build Include="dbo\Tables\User\AspNetRoles.sql" />
     
    100100    <Build Include="dbo\Tables\QuestionCategory\QuestionCategory.sql" />
    101101    <None Include="dbo\Tables\QuestionCategory\QuestionCategory.Debug.Seed.sql" />
     102    <Build Include="dbo\Tables\Vote\Vote.sql" />
    102103  </ItemGroup>
    103104  <ItemGroup>
  • src/FinkiChattery/FinkiChattery.Database/dbo/Tables/Answer/Answer.Debug.Seed.sql

    r1e0d869 r6901f8b  
    1212            (3, N'cee193c3-9d36-4ed8-81b2-15eb4ff305f1', N'Answer 3', 2, 1, 1, GETUTCDATE(), 5),
    1313            (4, N'dee193c3-9d36-4ed8-81b2-15eb4ff305f1', N'Answer 4', 2, 1, 0, GETUTCDATE(), 5)
    14     ) AS temp ([Id], [Uid], [Text], [QuestionFk], [StudentFk], [CorrectAnswer], [CreatedOn], [UpvotesCount])
     14    ) AS temp ([Id], [Uid], [Text], [QuestionFk], [StudentFk], [CorrectAnswer], [CreatedOn], [VotesCount])
    1515    ) AS S
    1616    ON T.[ID] = S.[ID]
     
    2121                   T.[StudentFk] = S.[StudentFk],
    2222                   T.[CorrectAnswer] = S.[CorrectAnswer],
    23                    T.[UpvotesCount] = S.[UpvotesCount]
     23                   T.[VotesCount] = S.[VotesCount]
    2424    WHEN NOT MATCHED THEN
    2525        INSERT
     
    3232            [CorrectAnswer],
    3333            [CreatedOn],
    34             [UpvotesCount]
     34            [VotesCount]
    3535        )
    3636        VALUES
    37         (S.[Id], S.[Uid], S.[Text], S.[QuestionFk], S.[StudentFk], S.[CorrectAnswer], S.[CreatedOn], S.[UpvotesCount]);
     37        (S.[Id], S.[Uid], S.[Text], S.[QuestionFk], S.[StudentFk], S.[CorrectAnswer], S.[CreatedOn], S.[VotesCount]);
    3838    SET IDENTITY_INSERT [dbo].[Answer] OFF
    3939END
  • src/FinkiChattery/FinkiChattery.Database/dbo/Tables/Answer/Answer.sql

    r1e0d869 r6901f8b  
    77    [CorrectAnswer] BIT              NOT NULL,
    88    [CreatedOn]     SMALLDATETIME    NOT NULL,
    9     [UpvotesCount] BIGINT NOT NULL DEFAULT 0,
     9    [VotesCount] BIGINT NOT NULL DEFAULT 0,
    1010    CONSTRAINT [PK_Answer] PRIMARY KEY CLUSTERED ([Id] ASC),
    1111    CONSTRAINT [FK_Answer_Question_QuestionFk] FOREIGN KEY ([QuestionFk]) REFERENCES [dbo].[Question] ([Id]),
  • src/FinkiChattery/FinkiChattery.Persistence/Configurations/AnswerConfig.cs

    r1e0d869 r6901f8b  
    2222            builder.Property(x => x.CorrectAnswer).HasColumnName(@"CorrectAnswer").HasColumnType("bit").IsRequired();
    2323            builder.Property(x => x.CreatedOn).HasColumnName(@"CreatedOn").HasColumnType("smalldatetime").IsRequired();
    24             builder.Property(x => x.UpvotesCount).HasColumnName(@"UpvotesCount").HasColumnType("bigint").IsRequired().HasDefaultValue(0);
     24            builder.Property(x => x.VotesCount).HasColumnName(@"VotesCount").HasColumnType("bigint").IsRequired().HasDefaultValue(0);
    2525
    2626            builder.HasOne(x => x.Question).WithMany(x => x.Answers).HasForeignKey(x => x.QuestionFk).OnDelete(DeleteBehavior.Restrict);
  • src/FinkiChattery/FinkiChattery.Persistence/Context/ApplicationDbContext.cs

    r1e0d869 r6901f8b  
    2323        public DbSet<TeacherTeam> TeacherTeams { get; set; }
    2424        public DbSet<Team> Teams { get; set; }
    25         public DbSet<Upvote> Upvotes { get; set; }
     25        public DbSet<Vote> Votes { get; set; }
    2626
    2727        protected override void OnModelCreating(ModelBuilder builder)
     
    4242            builder.ApplyConfiguration(new TeacherTeamConfig(schema));
    4343            builder.ApplyConfiguration(new TeamConfig(schema));
    44             builder.ApplyConfiguration(new UpvoteConfig(schema));
     44            builder.ApplyConfiguration(new VoteConfig(schema));
    4545        }
    4646    }
  • src/FinkiChattery/FinkiChattery.Persistence/Models/Answer.cs

    r1e0d869 r6901f8b  
    66    public class Answer : BaseEntity
    77    {
     8        public Answer() : base()
     9        {
     10            CreatedOn = DateTime.UtcNow;
     11            VotesCount = 0;
     12            CorrectAnswer = false;
     13        }
     14
    815        public string Text { get; set; }
    916
     
    2027        public DateTime CreatedOn { get; set; }
    2128
    22         public long UpvotesCount { get; set; }
     29        public long VotesCount { get; set; }
    2330
    24         public virtual ICollection<Upvote> Upvotes { get; set; }
     31        public virtual ICollection<Vote> Votes { get; set; }
    2532
    2633        public virtual ICollection<AnswerResponse> AnswerResponses { get; set; }
  • src/FinkiChattery/FinkiChattery.Persistence/Models/Moderator.cs

    r1e0d869 r6901f8b  
    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/Models/Student.cs

    r1e0d869 r6901f8b  
    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using System.Text;
    5 using System.Threading.Tasks;
     1using System.Collections.Generic;
    62
    73namespace FinkiChattery.Persistence.Models
     
    2218
    2319        public virtual ICollection<Question> Questions { get; set; }
    24        
     20
    2521        public virtual ICollection<Answer> Answers { get; set; }
    26        
     22
    2723        public virtual ICollection<StudentTeam> StudentTeams { get; set; }
    2824    }
  • src/FinkiChattery/FinkiChattery.Persistence/Models/Vote.cs

    r1e0d869 r6901f8b  
    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using System.Text;
    5 using System.Threading.Tasks;
     1using FinkiChattery.Persistence.Helpers;
    62
    73namespace FinkiChattery.Persistence.Models
    84{
    9     public class Upvote : BaseEntity
     5    public class Vote : BaseEntity
    106    {
     7        public Vote() : base()
     8        {
     9        }
     10
    1111        public long StudentFk { get; set; }
    1212
     
    1616
    1717        public virtual Answer Answer { get; set; }
     18
     19        public VoteType VoteType { get; set; }
    1820    }
    1921}
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Contracts/IQuestionRepo.cs

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    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/Contracts/Question/QuestionStateDto.cs

    r1e0d869 r6901f8b  
    8484    public class AnswerQuestionStateDto
    8585    {
    86         public AnswerQuestionStateDto(long id, Guid uid, string text, bool correctAnswer, DateTime createdOn, long upvotesCount, AnswerStudentQuestionStateDto studentDto, IEnumerable<AnswerResponseQuestionStateDto> answerResponsesDto)
     86        public AnswerQuestionStateDto(long id, Guid uid, string text, bool correctAnswer, DateTime createdOn, long votesCount, AnswerStudentQuestionStateDto studentDto, IEnumerable<AnswerResponseQuestionStateDto> answerResponsesDto)
    8787        {
    8888            Id = id;
     
    9191            CorrectAnswer = correctAnswer;
    9292            CreatedOn = createdOn;
    93             UpvotesCount = upvotesCount;
     93            VotesCount = votesCount;
    9494            StudentDto = studentDto;
    9595            AnswerResponsesDto = answerResponsesDto;
     
    101101        public bool CorrectAnswer { get; }
    102102        public DateTime CreatedOn { get; }
    103         public long UpvotesCount { get; }
     103        public long VotesCount { get; }
    104104        public AnswerStudentQuestionStateDto StudentDto { get; }
    105105        public IEnumerable<AnswerResponseQuestionStateDto> AnswerResponsesDto { get; }
  • src/FinkiChattery/FinkiChattery.Persistence/Repositories/Implementations/QuestionRepo.cs

    r1e0d869 r6901f8b  
    8080                                                    y.CorrectAnswer,
    8181                                                    y.CreatedOn,
    82                                                     y.UpvotesCount,
     82                                                    y.VotesCount,
    8383                                                    new AnswerStudentQuestionStateDto(
    8484                                                        y.Student.Id,
     
    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

    r1e0d869 r6901f8b  
    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

    r1e0d869 r6901f8b  
    1111        IStudentRepo Students { get; }
    1212        ITeamRepo Teams { get; }
     13        IVoteRepo Votes { get; }
     14        IAnswerRepo Answers { get; }
     15        ITeacherRepo Teachers { get; }
     16        IModeratorRepo Moderators { get; }
    1317        Task<int> SaveAsync();
    1418    }
  • src/FinkiChattery/FinkiChattery.Persistence/UnitOfWork/Implementations/UnitOfWork.cs

    r1e0d869 r6901f8b  
    1212        private StudentRepo _students;
    1313        private TeamRepo _teams;
     14        private VoteRepo _votes;
     15        private AnswerRepo _answers;
     16        private ModeratorRepo _moderators;
     17        private TeacherRepo _teachers;
    1418
    1519        public UnitOfWork(ApplicationDbContext dbContext)
    1620        {
    1721            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            }
    1848        }
    1949
     
    70100        }
    71101
     102        public IVoteRepo Votes
     103        {
     104            get
     105            {
     106                if (_votes == null)
     107                {
     108                    _votes = new VoteRepo(DbContext);
     109                }
     110
     111                return _votes;
     112            }
     113        }
     114
     115        public IAnswerRepo Answers
     116        {
     117            get
     118            {
     119                if (_answers == null)
     120                {
     121                    _answers = new AnswerRepo(DbContext);
     122                }
     123
     124                return _answers;
     125            }
     126        }
     127
    72128        public ApplicationDbContext DbContext { get; }
    73129
Note: See TracChangeset for help on using the changeset viewer.