diff --git a/quiz/models.py b/quiz/models.py index 92a6a50..f2d7506 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -1,8 +1,11 @@ +from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from django.db.models import Max, Sum from django.urls import reverse from jsonfield import JSONField +from jobboard.helpers import BaseAction + class Category(models.Model): title = models.CharField(max_length=255) @@ -57,17 +60,20 @@ def __str__(self): @property def max_points(self): - if self.kind.one_right_answer: - max_points = self.answers.all().aggregate(max=Max('weight'))['max'] * self.weight - else: - max_points = self.answers.filter(weight__gt=0).aggregate(sum=Sum('weight'))['sum'] * self.weight - return max_points + if self.kind: + if self.kind.one_right_answer: + max_points = self.answers.all().aggregate(max=Max('weight'))['max'] * self.weight + else: + max_points = self.answers.filter(weight__gt=0).aggregate(sum=Sum('weight'))['sum'] * self.weight + return max_points + return 0 class Answer(models.Model): text = models.TextField(verbose_name=u'Answer\'s text') weight = models.SmallIntegerField(default=1, - help_text=u'Value from -100 to 100') + help_text=u'Value from -100 to 100', + validators=[MinValueValidator(-100), MaxValueValidator(100)]) question = models.ForeignKey(Question, related_name='answers', on_delete=models.CASCADE) @@ -79,11 +85,11 @@ class Meta: ordering = ('?',) -class ActionExam(models.Model): +class ActionExam(BaseAction, models.Model): action = models.OneToOneField('pipeline.Action', - on_delete=models.SET_NULL, - null=True, - related_name='exam') + on_delete=models.CASCADE, + null=False, + related_name='exam') questions = models.ManyToManyField(Question) max_attempts = models.PositiveIntegerField(default=3) passing_grade = models.PositiveIntegerField(default=0) @@ -92,15 +98,28 @@ class ActionExam(models.Model): def __str__(self): return 'Exam for "{}"'.format(self.action) - def get_absolute_url(self): + def get_candidate_url(self): return reverse('candidate_examining', kwargs={'pk': self.pk}) + def get_result_url(self, **kwargs): + return reverse('exam_result', kwargs={'exam_id': self.id, 'candidate_id': kwargs.get('candidate_id')}) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + if self.id: + if self.questions.count() == 0: + return self.delete() + super().save(force_insert, force_update, using, update_fields) + + class Meta: + abstract = False + class ExamPassed(models.Model): - profile = models.ForeignKey('candidateprofile.CandidateProfile', - on_delete=models.SET_NULL, - null=True, - related_name='exams') + candidate = models.ForeignKey('jobboard.Candidate', + on_delete=models.SET_NULL, + null=True, + related_name='exams') exam = models.ForeignKey(ActionExam, on_delete=models.SET_NULL, null=True) @@ -111,10 +130,7 @@ class ExamPassed(models.Model): updated_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return '{}'.format(self.profile.candidate.contract_address) - - def get_absolute_url(self): - return reverse('exam_results', kwargs={'pk': self.id}) + return '{} exam result'.format(self.candidate.contract_address) class AnswerForVerification(models.Model): diff --git a/quiz/signals/handlers.py b/quiz/signals/handlers.py index 82708dd..cf6369d 100644 --- a/quiz/signals/handlers.py +++ b/quiz/signals/handlers.py @@ -2,23 +2,25 @@ from django.dispatch import receiver from jobboard.handlers.oracle import OracleHandler -from jobboard.tasks import save_txn_to_history from quiz.models import ExamPassed, AnswerForVerification, ActionExam from quiz.tasks import ProcessExam, VerifyAnswer +from pipeline.tasks import candidate_level_up @receiver(post_save, sender=ExamPassed) def candidate_pass_exam(sender, instance, created, **kwargs): - if created: + if created or not instance.processed: pr = ProcessExam() pr.delay(instance.id) else: if instance.processed and instance.points >= instance.exam.passing_grade: oracle = OracleHandler() - print(oracle.current_cv_action_on_vacancy(instance.exam.action.pipeline.vacancy.uuid, instance.cv.uuid)) - txn_hash = oracle.level_up(instance.exam.action.pipeline.vacancy.uuid, instance.cv.uuid) - save_txn_to_history.delay(instance.cv.candidate.user.id, txn_hash.hex(), - 'Level up on vacancy {}'.format(instance.exam.action.pipeline.vacancy.title)) + cci = oracle.get_candidate_current_action_index(instance.exam.action.pipeline.vacancy.uuid, + instance.candidate.contract_address) + if cci == instance.exam.action.index: + action_bch = oracle.get_action(instance.exam.action.pipeline.vacancy.uuid, cci) + if not action_bch['approvable']: + candidate_level_up.delay(instance.exam.action.pipeline.vacancy.id, instance.candidate.id) @receiver(pre_save, sender=AnswerForVerification) diff --git a/quiz/views.py b/quiz/views.py index 90691fc..f92b936 100644 --- a/quiz/views.py +++ b/quiz/views.py @@ -1,13 +1,13 @@ -from django.contrib import messages -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy, reverse from django.views import View from django.views.generic import TemplateView, CreateView, DetailView, ListView, UpdateView from django.views.generic.edit import BaseUpdateView -from candidateprofile.models import CandidateProfile +from jobboard.handlers.oracle import OracleHandler from jobboard.mixins import OnlyEmployerMixin, OnlyCandidateMixin +from jobboard.models import Candidate from pipeline.models import Action from quiz.forms import CategoryForm from quiz.models import ActionExam, Category, Question, Answer, QuestionKind, ExamPassed, AnswerForVerification @@ -27,7 +27,7 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def get_queryset(self): - return self.action.exam.first() + return hasattr(self.action, 'exam') and self.action.exam or None def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -41,6 +41,7 @@ class ActionAddQuestionsView(ListView): def __init__(self, **kwargs): super().__init__(**kwargs) self.action = None + self.request = None def dispatch(self, request, *args, **kwargs): self.action = get_object_or_404(Action, id=kwargs.get('pk')) @@ -52,26 +53,23 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['action'] = self.action - context['selected'] = self.get_seleted_exam_questions() + if hasattr(self.action, 'exam'): + context['selected'] = self.get_seleted_exam_questions() return context def get_seleted_exam_questions(self): - exam = self.action.exam.first() - if exam is not None: - return [qe.id for qe in exam.questions.all()] - return [] + return [qe.id for qe in self.action.exam.questions.all()] def post(self, request, *args, **kwargs): - action = get_object_or_404(Action, pipeline__vacancy__company__employer=request.role_object, + action = get_object_or_404(Action, id=request.POST.get('action')) + if not action.owner == request.user: + raise Http404 question_ids = request.POST.getlist('questions') - if question_ids: - action_exam, _ = ActionExam.objects.get_or_create(action=action) - action_exam.questions.set(Question.objects.filter(id__in=question_ids)) - action_exam.save() - else: - messages.error(request, 'You cannot delete all questions from exam.') - return redirect('action_exam', pk=action.id) + action_exam, _ = ActionExam.objects.get_or_create(action=action) + action_exam.questions.set(Question.objects.filter(id__in=question_ids)) + action_exam.save() + return redirect('action_details', pk=action.id) class CandidateExaminingView(OnlyCandidateMixin, TemplateView): @@ -87,22 +85,21 @@ def __init__(self, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.already_pass_exam: - context['exam_passed'] = ExamPassed.objects.filter(profile=self.profile, + context['exam_passed'] = ExamPassed.objects.filter(candidate=self.request.role_object, exam=self.action_exam).first() else: - context['profile'] = self.profile + context['candidate'] = self.request.role_object context['exam'] = self.action_exam context['action'] = self.action_exam.action return context def get(self, request, *args, **kwargs): - self.profile = get_object_or_404(CandidateProfile, pk=self.request.role_object.profile.id) self.action_exam = get_object_or_404(ActionExam, pk=kwargs.get('pk')) self.check_candidate() return super().get(self, request, *args, **kwargs) def check_candidate(self): - self.already_pass_exam = ExamPassed.objects.filter(profile=self.profile, + self.already_pass_exam = ExamPassed.objects.filter(candidate=self.request.role_object, exam=self.action_exam).exists() def post(self, request, *args, **kwargs): @@ -112,10 +109,9 @@ def post(self, request, *args, **kwargs): def process_request(self, request): self.action_exam = get_object_or_404(ActionExam, pk=request.POST.get('exam_id', None)) - self.profile = get_object_or_404(CandidateProfile, pk=request.POST.get('profile_id', None)) answers = {key: value[0] if len(value) == 1 else value for key, value in dict(request.POST).items() if key.startswith('question_') and value[0] != ''} - ExamPassed.objects.create(profile=self.profile, exam=self.action_exam, answers=answers) + ExamPassed.objects.create(candidate=request.role_object, exam=self.action_exam, answers=answers) class QuizIndexPage(TemplateView): @@ -136,15 +132,16 @@ class NewCategoryView(CreateView): form_class = CategoryForm success_url = reverse_lazy('quiz_index') + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.request = None + self.object = None + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['employer'] = self.request.role_object return kwargs - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.object = None - def form_valid(self, form): self.object = form.save(commit=False) self.object.employer = self.request.role_object @@ -265,7 +262,7 @@ def form_valid(self, form): return HttpResponse(True, status=200) def form_invalid(self, form): - return HttpResponse(False, status=403) + return HttpResponse(False, status=400) class ProcessAnswerView(View): @@ -282,5 +279,34 @@ def post(self, request, *args, **kwargs): return HttpResponse('ok', status=200) -class ExamResultsView(DetailView): +class ExamPassedView(OnlyEmployerMixin, DetailView): model = ExamPassed + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.action_exam = None + self.candidate = None + + def dispatch(self, request, *args, **kwargs): + self.action_exam = get_object_or_404(ActionExam, pk=kwargs.get('exam_id')) + if self.action_exam.action.owner != request.user: + raise Http404 + self.candidate = get_object_or_404(Candidate, pk=kwargs.get('candidate_id')) + return super().dispatch(request, *args, **kwargs) + + def get_object(self, queryset=None): + try: + results = ExamPassed.objects.get(exam=self.action_exam, candidate=self.candidate) + except ExamPassed.DoesNotExist: + results = {} + return results + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['action'] = self.action_exam.action + context['candidate'] = self.candidate + oracle = OracleHandler() + cci = oracle.get_candidate_current_action_index(self.action_exam.action.pipeline.vacancy.uuid, + self.candidate.contract_address) + cci == self.action_exam.action.index and context.update({'not_yet': True}) + return context diff --git a/requirements.txt b/requirements.txt index f607b48..545af2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ amqp==2.2.2 appnope==0.1.0 attrdict==2.0.0 +authy==2.1.5 billiard==3.5.0.3 celery==4.1.0 certifi==2018.1.18 @@ -63,6 +64,7 @@ scikit-learn==0.19.1 scipy==1.0.0 semantic-version==2.6.0 simplegeneric==0.8.1 +simplejson==3.16.0 six==1.11.0 sklearn==0.0 toolz==0.9.0