├── source ├── course_interviews │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_views.py │ │ ├── test_admin.py │ │ └── test_manage.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── generate_students.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0006_student_has_received_new_courses_email.py │ │ ├── 0008_auto_20151104_1344.py │ │ ├── 0003_auto_20151029_1312.py │ │ ├── 0004_auto_20151029_1339.py │ │ ├── 0007_auto_20151103_1446.py │ │ ├── 0005_auto_20151030_1101.py │ │ ├── 0002_auto_20151028_1926.py │ │ └── 0001_initial.py │ ├── static │ │ ├── images │ │ │ ├── competition.gif │ │ │ ├── hackbulgaria_logo.png │ │ │ └── quality_education.gif │ │ ├── bower.json │ │ ├── js │ │ │ └── choose_new_interview.js │ │ └── css │ │ │ └── styles.css │ ├── serializers.py │ ├── helpers │ │ ├── get_students.py │ │ ├── get_free_interview_slots.py │ │ ├── generate_interviews.py │ │ ├── course_students.py │ │ ├── generate_interview_slots.py │ │ ├── get_students_emails.py │ │ ├── applicant.py │ │ ├── generate_students.py │ │ └── generate_emails.py │ ├── views.py │ ├── templates │ │ ├── admin │ │ │ └── course_interviews │ │ │ │ └── interviewslot │ │ │ │ └── change_list.html │ │ ├── index.html │ │ ├── confirm_interview.html │ │ ├── base.html │ │ └── choose_interview.html │ ├── urls.py │ ├── apis.py │ ├── models.py │ └── admin.py ├── interview_communicator │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ ├── example_local_settings.py │ └── settings.py └── manage.py ├── requirements.txt ├── README.md ├── .coverage.rc ├── .travis.yml ├── deploy ├── Capfile └── config │ ├── deploy.rb │ └── deploy │ └── production.rb └── .gitignore /source/course_interviews/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/course_interviews/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/interview_communicator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/course_interviews/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/course_interviews/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/course_interviews/static/images/competition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackBulgaria/InterviewSystem/HEAD/source/course_interviews/static/images/competition.gif -------------------------------------------------------------------------------- /source/course_interviews/static/images/hackbulgaria_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackBulgaria/InterviewSystem/HEAD/source/course_interviews/static/images/hackbulgaria_logo.png -------------------------------------------------------------------------------- /source/course_interviews/static/images/quality_education.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackBulgaria/InterviewSystem/HEAD/source/course_interviews/static/images/quality_education.gif -------------------------------------------------------------------------------- /source/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "interview_communicator.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==3.7.1 2 | coveralls==0.5 3 | Django==1.8.5 4 | django-ckeditor==5.0.2 5 | django-extensions==1.5.7 6 | django-phonenumber-field==0.7.2 7 | django-post-office==2.0.3 8 | django-suit==0.2.15 9 | jsonfield==1.0.3 10 | phonenumbers==7.1.0 11 | requests==2.8.1 12 | six==1.10.0 13 | wheel==0.24.0 14 | psycopg2==2.6.1 15 | -------------------------------------------------------------------------------- /source/course_interviews/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import InterviewSlot 3 | 4 | 5 | class InterviewSlotSerializer(serializers.ModelSerializer): 6 | date = serializers.ReadOnlyField(source='teacher_time_slot.date') 7 | 8 | class Meta: 9 | model = InterviewSlot 10 | fields = ('id', 'date', 'start_time') 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Application for Hack Bulgaria's interviews 2 | [![Build Status](https://travis-ci.org/HackBulgaria/InterviewSystem.svg?branch=master)](https://travis-ci.org/HackBulgaria/InterviewSystem) 3 | [![Coverage Status](https://coveralls.io/repos/HackBulgaria/InterviewSystem/badge.svg?branch=master&service=github)](https://coveralls.io/github/HackBulgaria/InterviewSystem?branch=master) 4 | -------------------------------------------------------------------------------- /.coverage.rc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | source/*/migrations/* 4 | source/*/tests.py 5 | source/*/__init__.py 6 | source/manage.py 7 | source/wsgi.py 8 | source/example_local_settings.py 9 | exclude_lines = 10 | class Command 11 | __init__ 12 | pragma: no cover 13 | def __repr__ 14 | raise AssertionError 15 | raise NotImplementedError 16 | if __name__ == .__main__.: 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.4' 4 | addons: 5 | postgresql: '9.3' 6 | services: postgresql 7 | install: 8 | - pip install -r requirements.txt --use-mirrors 9 | before_script: 10 | - psql -c 'CREATE DATABASE testdb;' -U postgres 11 | script: 12 | - mv source/interview_communicator/example_local_settings.py source/interview_communicator/local_settings.py 13 | - cd source 14 | - python manage.py migrate 15 | - coverage run --source='.' manage.py test 16 | after_success: 17 | - coveralls --rcfile=.coverage.rc -------------------------------------------------------------------------------- /source/interview_communicator/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for interview_communicator project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "interview_communicator.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0006_student_has_received_new_courses_email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0005_auto_20151030_1101'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='student', 16 | name='has_received_new_courses_email', 17 | field=models.BooleanField(default=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0008_auto_20151104_1344.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0007_auto_20151103_1446'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='interviewslot', 16 | name='student', 17 | field=models.OneToOneField(to='course_interviews.Student', blank=True, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/get_students.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class AbstractGetStudents: 5 | 6 | def __init__(self, address, form_name, api_key, count, page): 7 | self.address = address 8 | self.form_name = form_name 9 | self.api_key = api_key 10 | self.count = count 11 | self.page = page 12 | 13 | def get_student_applications(self): 14 | url = self.address + self.form_name + "/applications?api_key=" \ 15 | + self.api_key + "&page=" + str(self.page) + "&count=" + str(self.count) 16 | applications = requests.get(url).json() 17 | return applications 18 | -------------------------------------------------------------------------------- /source/course_interviews/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Interview-Communicator", 3 | "homepage": "https://github.com/PavlinGergov/Django-Projects", 4 | "authors": [ 5 | "Pavlin Gergov " 6 | ], 7 | "description": "", 8 | "main": "", 9 | "moduleType": [], 10 | "license": "MIT", 11 | "private": true, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "dependencies": { 20 | "jquery": "~2.1.4", 21 | "bootstrap": "~3.3.5", 22 | "handlebars": "~4.0.3", 23 | "font-awesome": "fontawesome#~4.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0003_auto_20151029_1312.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0002_auto_20151028_1926'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='teacher', 16 | name='first_name', 17 | field=models.CharField(default=None, max_length=50), 18 | ), 19 | migrations.AddField( 20 | model_name='teacher', 21 | name='last_name', 22 | field=models.CharField(default=None, max_length=50), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0004_auto_20151029_1339.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0003_auto_20151029_1312'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='teacher', 16 | name='first_name', 17 | field=models.CharField(max_length=50, default=''), 18 | ), 19 | migrations.AlterField( 20 | model_name='teacher', 21 | name='last_name', 22 | field=models.CharField(max_length=50, default=''), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0007_auto_20151103_1446.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0006_student_has_received_new_courses_email'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='interviewerfreetime', 16 | name='buffer_time', 17 | field=models.BooleanField(default=False), 18 | ), 19 | migrations.AddField( 20 | model_name='interviewslot', 21 | name='buffer_slot', 22 | field=models.BooleanField(default=False), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /source/course_interviews/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404 2 | from django.http import Http404 3 | from .models import Student 4 | from .apis import * 5 | 6 | 7 | def index(request): 8 | return render(request, "index.html", locals()) 9 | 10 | 11 | def confirm_interview(request, token): 12 | student = get_object_or_404(Student, uuid=token) 13 | teacher_skype = student.interviewslot.teacher_time_slot.teacher.skype 14 | 15 | return render(request, "confirm_interview.html", locals()) 16 | 17 | 18 | def choose_interview(request, token): 19 | student = get_object_or_404(Student, uuid=token) 20 | 21 | if student.has_confirmed_interview: 22 | raise Http404("Student already has an interview date") 23 | 24 | return render(request, "choose_interview.html", locals()) 25 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/get_free_interview_slots.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import InterviewSlot 2 | from datetime import datetime 3 | 4 | 5 | def get_free_interview_slots(): 6 | # Buffer slots are provided specially for the 7 | # students that can not attend their initial interview date 8 | 9 | # Buffer slots 10 | available_slots = InterviewSlot.objects.all().filter( 11 | buffer_slot=True, student__isnull=True).order_by('teacher_time_slot__date', 'start_time') 12 | 13 | # If no bufers are left, use other available empty slots after the current day 14 | if len(available_slots) == 0: 15 | today = datetime.now() 16 | available_slots = InterviewSlot.objects.all().filter( 17 | buffer_slot=False, student__isnull=True, teacher_time_slot__date__gt=datetime.date( 18 | today)).order_by('teacher_time_slot__date', 'start_time') 19 | 20 | return available_slots 21 | -------------------------------------------------------------------------------- /source/course_interviews/templates/admin/course_interviews/interviewslot/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% block object-tools-items %} 3 | 4 | {% if user.is_superuser %} 5 | 6 | 7 | Generate slots 8 | 9 | 10 | 11 | 12 | 13 | Generate interviews 14 | 15 | 16 | 17 | 18 | 19 | Generate emails 20 | 21 | 22 | {% endif %} 23 | 24 | {{ block.super }} 25 | 26 | {% endblock %} 27 | 28 | -------------------------------------------------------------------------------- /deploy/Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Include tasks from other gems included in your Gemfile 8 | # 9 | # For documentation on these, see for example: 10 | # 11 | # https://github.com/capistrano/rvm 12 | # https://github.com/capistrano/rbenv 13 | # https://github.com/capistrano/chruby 14 | # https://github.com/capistrano/bundler 15 | # https://github.com/capistrano/rails 16 | # https://github.com/capistrano/passenger 17 | # 18 | # require 'capistrano/rvm' 19 | # require 'capistrano/rbenv' 20 | # require 'capistrano/chruby' 21 | # require 'capistrano/bundler' 22 | # require 'capistrano/rails/assets' 23 | # require 'capistrano/rails/migrations' 24 | # require 'capistrano/passenger' 25 | 26 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 27 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 28 | -------------------------------------------------------------------------------- /source/interview_communicator/urls.py: -------------------------------------------------------------------------------- 1 | """interview_communicator URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', include(admin.site.urls)), 21 | url(r'^', include('course_interviews.urls', namespace='course_interviews')) 22 | ] 23 | -------------------------------------------------------------------------------- /source/course_interviews/static/js/choose_new_interview.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | refreshInterviews() 3 | 4 | window.setInterval(function(){ 5 | refreshInterviews() 6 | }, 5000); 7 | }); 8 | 9 | window.slotId = false; 10 | 11 | function addOnClick(){ 12 | $(".choose-interview").click(function(){ 13 | window.slotId = $(this).data('slotid') 14 | }); 15 | } 16 | 17 | function drawTable(data){ 18 | $("#interview-slots-table > tbody").empty(); 19 | var source = $("#interview-slots-template").html(); 20 | var template = Handlebars.compile(source); 21 | data.forEach(function(slot){ 22 | var html = template(slot); 23 | $("#interview-slots-table > tbody").append(html); 24 | }); 25 | } 26 | 27 | function refreshInterviews(){ 28 | $.ajax({ 29 | url: "/api/get-interview-slots/", 30 | type: 'GET', 31 | success: function(res) { 32 | drawTable(res) 33 | addOnClick() 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /source/course_interviews/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import (index, get_students, get_emails, get_all_emails, confirm_interview, 4 | choose_interview, confirm_slot, get_interview_slots, confirm_student_interview) 5 | 6 | urlpatterns = [ 7 | url(r'^$', index, name='index'), 8 | url(r'^api/get-students/(?P[-\w]+)/$', get_students, name="get_students"), 9 | url(r'^api/get-emails/$', get_emails, name="get_emails"), 10 | url(r'^api/get-all-emails/$', get_all_emails, name="get_all_emails"), 11 | url(r'^api/get-interview-slots/$', get_interview_slots, name="get_interview_slots"), 12 | url(r'^confirm-interview/(?P[-\w]+)/$', confirm_interview, name="confirm_interview"), 13 | url(r'^choose-interview/(?P[-\w]+)/$', choose_interview, name="choose_interview"), 14 | url(r'^confirm-slot/$', confirm_slot, name="confirm_slot"), 15 | url(r'^confirm-student-interview/$', confirm_student_interview, name="confirm_student_interview"), 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | *.idea 57 | *.iml 58 | 59 | db.sqlite3 60 | local_settings.py 61 | /static/ 62 | media/ 63 | bower_components 64 | .sass-cache/ 65 | *.map 66 | *.swp 67 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0005_auto_20151030_1101.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('course_interviews', '0004_auto_20151029_1339'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='student', 16 | name='applied_course', 17 | field=models.CharField(blank=True, max_length=1100, null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='student', 21 | name='skype', 22 | field=models.CharField(max_length=1100, default=None), 23 | ), 24 | migrations.AlterField( 25 | model_name='student', 26 | name='studies_at', 27 | field=models.CharField(blank=True, max_length=1100, null=True), 28 | ), 29 | migrations.AlterField( 30 | model_name='student', 31 | name='works_at', 32 | field=models.CharField(blank=True, max_length=1100, null=True), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /source/course_interviews/management/commands/generate_students.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from course_interviews.helpers.generate_students import GenerateStudents 3 | from interview_communicator.local_settings import f6s_address, f6s_application_name, f6s_api_key, f6s_page_count, f6s_page 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Generate students(applicants) with finalized forms by making a request to f6s' 8 | 9 | def handle(self, **options): 10 | 11 | students_generator = GenerateStudents( 12 | f6s_address, f6s_application_name, f6s_api_key, f6s_page_count, f6s_page) 13 | 14 | students_generator.generate_students() 15 | 16 | generated_students = students_generator.get_generated_students() 17 | finalized_students = students_generator.get_students_with_finalized_applications() 18 | students_in_base = students_generator.get_students_in_base() 19 | 20 | print(str(generated_students) + ' students were generated') 21 | print('There are ' + str( 22 | finalized_students) + ' students with finalized applications and ' + str( 23 | students_in_base) + ' in the base') 24 | -------------------------------------------------------------------------------- /source/course_interviews/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load staticfiles %} 3 | 4 | {% block 'page-content' %} 5 | {% autoescape off %} 6 |
7 |

8 | Next week will start the second stage of the HackBulgaria's Fall 2015 courses campaign - tech interviews! 9 |

10 |

11 | Everyone who had applied will receive an email with information & details about the interviews. 12 |

13 |

14 | So check your email! ;) 15 |

16 |

17 | Here are our tips for interviews: 18 |

19 |
    20 |
  • 21 |

    22 | Make sure you’re rested and ready. 23 |

    24 |
  • 25 |
  • 26 |

    Ensure that your internet connection is stable during the Skype interview

    27 |
  • 28 |
  • 29 |

    Check your code - we will ask you questions about your solution's implementation

    30 |
  • 31 |
  • 32 |

    Keep calm :)

    33 |
  • 34 |
  • 35 |

    Manage well your time

    36 |
  • 37 |
38 |
39 | 40 |
41 |
42 | {% endautoescape %} 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /source/course_interviews/static/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | background: #F2F2F2; 9 | height: 100%; 10 | } 11 | 12 | body #page-wrap { 13 | min-height: 100%; 14 | position: relative; 15 | } 16 | 17 | body #page-wrap #header nav { 18 | height: 80px; 19 | background: black; 20 | } 21 | 22 | body #page-wrap #header img { 23 | max-height: 50px; 24 | max-width: 100%; 25 | float: left; 26 | padding-left: 10px; 27 | } 28 | 29 | body #content { 30 | padding-bottom: 100px; 31 | } 32 | 33 | body #content .choose-interview-container h2{ 34 | text-align: center; 35 | } 36 | 37 | body #footer { 38 | background: #F7931E; 39 | color: white; 40 | text-align: center; 41 | 42 | width: 100%; 43 | height: 100px; 44 | position: absolute; 45 | bottom: 0; 46 | left: 0; 47 | } 48 | 49 | body #footer #socials { 50 | padding: 10px 0 10px 0; 51 | } 52 | 53 | body #footer #socials a { 54 | padding: 5px; 55 | text-decoration: none; 56 | color: white; 57 | } 58 | 59 | body .image-container img { 60 | display:block; 61 | margin:auto; 62 | } 63 | 64 | body .confirm-text-container h1 { 65 | text-align: center; 66 | } 67 | 68 | .modal-title { 69 | text-align: center; 70 | } 71 | 72 | .modal-body { 73 | text-align: center; 74 | } -------------------------------------------------------------------------------- /source/course_interviews/helpers/generate_interviews.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student, InterviewSlot 2 | from datetime import datetime 3 | 4 | 5 | class GenerateInterviews: 6 | 7 | def __init__(self): 8 | self.__students_without_interviews = 0 9 | self.__generated_interviews = 0 10 | 11 | def __inc_generated_interviews(self): 12 | self.__generated_interviews += 1 13 | 14 | def generate_interviews(self): 15 | students = list(Student.objects.all()) 16 | slots = InterviewSlot.objects.all() 17 | today = datetime.now() 18 | for slot in slots: 19 | if slot.student or slot.buffer_slot or slot.teacher_time_slot.date <= datetime.date(today): 20 | continue 21 | while len(students) != 0: 22 | student = students.pop(0) 23 | if not student.has_interview_date: 24 | self.__inc_generated_interviews() 25 | slot.student = student 26 | student.has_interview_date = True 27 | slot.save() 28 | student.save() 29 | break 30 | 31 | def get_students_without_interviews(self): 32 | return Student.objects.all().filter(interviewslot__isnull=True).count() 33 | 34 | def get_generated_interviews_count(self): 35 | return self.__generated_interviews 36 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0002_auto_20151028_1926.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('auth', '0006_require_contenttypes_0002'), 11 | ('course_interviews', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='teacher', 17 | name='groups', 18 | field=models.ManyToManyField(help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', to='auth.Group', verbose_name='groups', related_query_name='user', blank=True, related_name='user_set'), 19 | ), 20 | migrations.AddField( 21 | model_name='teacher', 22 | name='is_superuser', 23 | field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), 24 | ), 25 | migrations.AddField( 26 | model_name='teacher', 27 | name='user_permissions', 28 | field=models.ManyToManyField(help_text='Specific permissions for this user.', to='auth.Permission', verbose_name='user permissions', related_query_name='user', blank=True, related_name='user_set'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /source/interview_communicator/example_local_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | # SECURITY WARNING: keep the secret key used in production secret! 6 | SECRET_KEY = '14u_t_2p+*4r6jyj(!zg48h1ifbqga+q00szndh4rx-ce47yhb3' 7 | 8 | 9 | # SECURITY WARNING: don't run with debug turned on in production! 10 | DEBUG = True 11 | 12 | ALLOWED_HOSTS = [] 13 | 14 | # Database 15 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 16 | 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 20 | 'NAME': 'testdb', 21 | 'USER': 'postgres', 22 | 'HOST': 'localhost', 23 | 'PORT': '', 24 | } 25 | } 26 | 27 | 28 | # Static files (CSS, JavaScript, Images) 29 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 30 | STATIC_URL = '/static/' 31 | STATIC_ROOT = os.path.join(BASE_DIR, '..', 'static') 32 | MEDIA_URL = '/media/' 33 | MEDIA_ROOT = os.path.join(BASE_DIR, '..', 'media/') 34 | 35 | confirm_interview_url = "" 36 | choose_interview_url = "" 37 | 38 | EMAIL_USE_TLS = True 39 | EMAIL_HOST = '' 40 | EMAIL_PORT = 123 41 | EMAIL_HOST_USER = 'hackbulgaria@gmail.com' 42 | EMAIL_HOST_PASSWORD = '' 43 | DEFAULT_FROM_EMAIL = EMAIL_HOST_USER 44 | 45 | f6s_address = "https://api.f6s.com/" 46 | f6s_application_name = "" 47 | f6s_api_key = "" 48 | f6s_page_count = 100 49 | f6s_page = 1 50 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/course_students.py: -------------------------------------------------------------------------------- 1 | from .get_students import AbstractGetStudents 2 | 3 | 4 | class CourseStudents(AbstractGetStudents): 5 | 6 | def __init__(self, address, form_name, api_key, count, page, course): 7 | super().__init__(address, form_name, api_key, count, page) 8 | self.course = course 9 | self.__json = { 10 | "item": 0, 11 | "min": { 12 | "value": 0 13 | }, 14 | "max": { 15 | "value": 0 16 | } 17 | } 18 | 19 | def generate_students_for_course(self): 20 | while (True): 21 | applications = self.get_student_applications() 22 | 23 | # Break loop if all students are added 24 | if applications["items_count"] == 0: 25 | break 26 | self.page += 1 27 | 28 | # If there is selected course and the application is 29 | # Finalized, inc() the students for the specified course 30 | for student in applications["data"]: 31 | if student["questions"][6]["field_response"] and \ 32 | student["questions"][6]["field_response"][0] == self.course: 33 | self.__json["max"]["value"] += 1 34 | if student["status"] == "Finalized": 35 | self.__json["item"] += 1 36 | 37 | def get_json(self): 38 | return self.__json 39 | -------------------------------------------------------------------------------- /source/course_interviews/templates/confirm_interview.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load staticfiles %} 3 | 4 | {% block 'page-content' %} 5 | {% autoescape off %} 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 |

Your interview is confirmed!

14 |

Your tech interview for the {{ student.applied_course }} course is scheduled for {{ student.interviewslot.teacher_time_slot.date }} at {{ student.interviewslot.start_time }}

15 |

The skype of your interviewer is {{ teacher_skype }}

16 |

If you are unable to attend your interview, please contact us at hackbulgaria@gmail.com

17 |
18 | 19 |
20 | {% endautoescape %} 21 | 22 | 38 | {% endblock %} 39 | {% block 'scripts' %} 40 | 60 | {% endblock %} -------------------------------------------------------------------------------- /deploy/config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for current version of Capistrano 2 | lock '3.4.0' 3 | 4 | set :application, 'interview' 5 | set :repo_url, 'https://github.com/HackBulgaria/InterviewSystem.git' 6 | 7 | # Default branch is :master 8 | ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp 9 | 10 | # Default deploy_to directory is /var/www/my_app_name 11 | set :deploy_to, '/hack/interview' 12 | 13 | # Default value for :scm is :git 14 | # set :scm, :git 15 | 16 | # Default value for :format is :pretty 17 | # set :format, :pretty 18 | 19 | # Default value for :log_level is :debug 20 | # set :log_level, :debug 21 | 22 | # Default value for :pty is false 23 | # set :pty, true 24 | 25 | # Default value for :linked_files is [] 26 | set :linked_files, fetch(:linked_files, []).push('source/interview_communicator/local_settings.py') 27 | 28 | # Default value for linked_dirs is [] 29 | set :linked_dirs, fetch(:linked_dirs, []).push('static', 'media') 30 | 31 | # Default value for default_env is {} 32 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 33 | 34 | # Default value for keep_releases is 5 35 | # set :keep_releases, 5 36 | 37 | namespace :deploy do 38 | task :pip_install do 39 | on roles(:all) do |h| 40 | execute "/hack/interview/shared/virtualenv/bin/pip install -r /hack/interview/current/requirements.txt" 41 | end 42 | end 43 | 44 | task :run_migrations do 45 | on roles(:all) do |h| 46 | execute "/hack/interview/shared/virtualenv/bin/python3 /hack/interview/current/source/manage.py migrate --noinput" 47 | end 48 | end 49 | 50 | task :bower_install do 51 | on roles(:all) do |h| 52 | execute "cd /hack/interview/current/source/course_interviews/static/ && bower install" 53 | end 54 | end 55 | 56 | task :run_collect_static do 57 | on roles(:all) do |h| 58 | execute "/hack/interview/shared/virtualenv/bin/python3 /hack/interview/current/source/manage.py collectstatic --noinput" 59 | end 60 | end 61 | 62 | task :restart do 63 | on roles(:all) do |h| 64 | execute "sudo restart interview" 65 | end 66 | end 67 | 68 | after :published, :pip_install 69 | after :pip_install, :run_migrations 70 | after :pip_install, :bower_install 71 | after :bower_install, :run_collect_static 72 | after :run_migrations, :restart 73 | end -------------------------------------------------------------------------------- /source/course_interviews/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | HackBulgaria Interviews 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 29 | 30 |
31 | {% block 'page-content' %} 32 | 33 | {% endblock %} 34 |
35 | 36 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | {% block 'scripts' %} 67 | 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /deploy/config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # server-based syntax 2 | # ====================== 3 | # Defines a single server with a list of roles and multiple properties. 4 | # You can define all roles on a single server, or split them: 5 | 6 | # server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value 7 | # server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value 8 | # server 'db.example.com', user: 'deploy', roles: %w{db} 9 | server '188.166.110.189', user: 'hack', roles: %w{web app}, my_property: :my_value 10 | 11 | # role-based syntax 12 | # ================== 13 | 14 | # Defines a role with one or multiple servers. The primary server in each 15 | # group is considered to be the first unless any hosts have the primary 16 | # property set. Specify the username and a domain or IP for the server. 17 | # Don't use `:all`, it's a meta role. 18 | 19 | # role :app, %w{deploy@example.com}, my_property: :my_value 20 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value 21 | # role :db, %w{deploy@example.com} 22 | role :app, %w{hack@188.166.110.189} 23 | role :web, %w{hack@188.166.110.189} 24 | role :db, %w{hack@188.166.110.189} 25 | 26 | 27 | 28 | # Configuration 29 | # ============= 30 | # You can set any configuration variable like in config/deploy.rb 31 | # These variables are then only loaded and set in this stage. 32 | # For available Capistrano configuration variables see the documentation page. 33 | # http://capistranorb.com/documentation/getting-started/configuration/ 34 | # Feel free to add new variables to customise your setup. 35 | 36 | 37 | 38 | # Custom SSH Options 39 | # ================== 40 | # You may pass any option but keep in mind that net/ssh understands a 41 | # limited set of options, consult the Net::SSH documentation. 42 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start 43 | # 44 | # Global options 45 | # -------------- 46 | # set :ssh_options, { 47 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 48 | # forward_agent: false, 49 | # auth_methods: %w(password) 50 | # } 51 | # 52 | # The server-based syntax can be used to override options: 53 | # ------------------------------------ 54 | # server 'example.com', 55 | # user: 'user_name', 56 | # roles: %w{web app}, 57 | # ssh_options: { 58 | # user: 'user_name', # overrides user setting above 59 | # keys: %w(/home/user_name/.ssh/id_rsa), 60 | # forward_agent: false, 61 | # auth_methods: %w(publickey password) 62 | # # password: 'please use keys' 63 | # } 64 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/generate_interview_slots.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import InterviewerFreeTime, InterviewSlot 2 | from datetime import datetime, timedelta 3 | 4 | 5 | class GenerateInterviewSlots: 6 | 7 | def __init__(self, interview_time_length, break_time): 8 | self.interview_time_length = interview_time_length 9 | self.break_time = break_time 10 | self.__slots_generated = 0 11 | 12 | def __inc_slots_generated(self): 13 | self.__slots_generated += 1 14 | 15 | def __calculate_diff_in_time(self, start_time, end_time): 16 | start_delta = timedelta( 17 | hours=start_time.hour, minutes=start_time.minute, seconds=start_time.second) 18 | end_delta = timedelta( 19 | hours=end_time.hour, minutes=end_time.minute, seconds=end_time.second) 20 | return (end_delta - start_delta).seconds / 60 21 | 22 | def generate_interview_slots(self): 23 | teacher_time_slots = InterviewerFreeTime.objects.all().order_by('date') 24 | 25 | for slot in teacher_time_slots: 26 | # Check if slots are already generated for that time_slot 27 | # (by a previous invocation of manage.py generate_slots) 28 | if slot.has_generated_slots(): 29 | continue 30 | 31 | # summarized free time of the interviewer 32 | free_time = self.__calculate_diff_in_time(slot.start_time, slot.end_time) 33 | # starting time of the first interview 34 | interview_start_time = slot.start_time 35 | 36 | while free_time >= self.interview_time_length: 37 | if slot.buffer_time: 38 | InterviewSlot.objects.create( 39 | teacher_time_slot=slot, 40 | start_time=interview_start_time, 41 | buffer_slot=True) 42 | else: 43 | InterviewSlot.objects.create( 44 | teacher_time_slot=slot, 45 | start_time=interview_start_time, 46 | buffer_slot=False) 47 | 48 | self.__inc_slots_generated() 49 | 50 | # Decrease the free time and change the starting time of the next interview 51 | free_time -= (self.interview_time_length + self.break_time) 52 | next_interview_date_and_time = datetime.combine( 53 | slot.date, interview_start_time) + timedelta( 54 | minutes=(self.interview_time_length + self.break_time)) 55 | interview_start_time = next_interview_date_and_time.time() 56 | 57 | def get_generated_slots(self): 58 | return self.__slots_generated 59 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/get_students_emails.py: -------------------------------------------------------------------------------- 1 | from .get_students import AbstractGetStudents 2 | 3 | 4 | # This class is helper for the api/get-emails view 5 | class GetStudentsEmails(AbstractGetStudents): 6 | 7 | def __init__(self, address, form_name, api_key, count, page, courses): 8 | super().__init__(address, form_name, api_key, count, page) 9 | self.courses = courses 10 | self.json = { 11 | "No course selected": [], 12 | } 13 | self.generate_courses_for_json() 14 | 15 | def generate_courses_for_json(self): 16 | for course in self.courses: 17 | self.json[course] = [] 18 | 19 | def get_validated_email(self, email): 20 | try: 21 | # F6S Where is your validation?!? 22 | email = email.split(" ")[1][13:-1].replace("@", "@").replace(",", ".") 23 | except: 24 | pass 25 | email = email.replace(",", ".") 26 | return email 27 | 28 | def generate_students_emails(self): 29 | while (True): 30 | applications = self.get_student_applications() 31 | 32 | # Break loop if all students are added 33 | if applications["items_count"] == 0: 34 | break 35 | self.page += 1 36 | 37 | for student in applications["data"]: 38 | email = self.get_validated_email(student["questions"][7]["question_response"]) 39 | # Check if application is still in progress and there is email 40 | if student["status"] == "In Progress" and email != "": 41 | # Check if there is selected course 42 | if student["questions"][6]["field_response"]: 43 | course = student["questions"][6]["field_response"][0] 44 | self.json[course].append(email) 45 | else: 46 | self.json["No course selected"].append(email) 47 | 48 | def get_json(self): 49 | return self.json 50 | 51 | 52 | # This class is helper for the api/get-all-emails view 53 | class GetAllStudentsEmails(GetStudentsEmails): 54 | 55 | def __init__(self, address, form_name, api_key, count, page, courses): 56 | super().__init__(address, form_name, api_key, count, page, courses) 57 | self.json = { 58 | "students": [] 59 | } 60 | 61 | def generate_students_emails(self): 62 | while (True): 63 | applications = self.get_student_applications() 64 | 65 | # Break loop if all students are added 66 | if applications["items_count"] == 0: 67 | break 68 | self.page += 1 69 | 70 | for student in applications["data"]: 71 | email = self.get_validated_email(student["questions"][7]["question_response"]) 72 | # Check if there is email 73 | if email != "": 74 | self.json["students"].append(email) 75 | 76 | def get_json(self): 77 | return self.json 78 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/applicant.py: -------------------------------------------------------------------------------- 1 | class Applicant: 2 | 3 | def __init__( 4 | self, name="", email="", skype="", phone_number="", applied_course="", 5 | first_task="", second_task="", third_task="", studies_at="", works_at="" 6 | ): 7 | self.name = name 8 | self.email = email 9 | self.skype = skype 10 | self.phone_number = phone_number 11 | self.applied_course = applied_course 12 | self.first_task = first_task 13 | self.second_task = second_task 14 | self.third_task = third_task 15 | self.studies_at = studies_at 16 | self.works_at = works_at 17 | 18 | def get_name(self): 19 | return self.name 20 | 21 | def get_email(self): 22 | # Example email returned from F6S: 23 | # "barcabg95@abv.bg<\/a>" 25 | try: 26 | self.email = self.email.split(" ")[1][13:-1].replace("@", "@") 27 | except: 28 | pass 29 | # F6S does not validate anything, so a wrong email comes as a string 30 | # Example: 31 | # "alexandermladjov@abv,bg" 32 | self.email = self.email.replace(",", ".") 33 | 34 | return self.email 35 | 36 | def get_skype(self): 37 | # If an email is passed instead of skype name (hot fix) 38 | if self.skype.startswith("github.com\/ 60 | # VladislavAtanasov\/HackBulgariaJavaCourseApplication\/blob\/master\/Task1\/Points.java<\/a>" 61 | def get_first_task(self): 62 | task = "" 63 | try: 64 | task = self.first_task.split(" ")[1][6:-1] 65 | except: 66 | pass 67 | return task 68 | 69 | def get_second_task(self): 70 | task = "" 71 | try: 72 | task = self.second_task.split(" ")[1][6:-1] 73 | except: 74 | pass 75 | return task 76 | 77 | def get_third_task(self): 78 | task = "" 79 | try: 80 | task = self.third_task.split(" ")[1][6:-1] 81 | except: 82 | pass 83 | return task 84 | 85 | def get_studies_at(self): 86 | return self.studies_at 87 | 88 | def get_works_at(self): 89 | return self.works_at 90 | -------------------------------------------------------------------------------- /source/interview_communicator/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS as TCP 2 | import os 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 9 | # Application definition 10 | 11 | INSTALLED_APPS = ( 12 | 'suit', 13 | 'django.contrib.admin', 14 | 'django.contrib.auth', 15 | 'django.contrib.contenttypes', 16 | 'django.contrib.sessions', 17 | 'django.contrib.messages', 18 | 'django.contrib.staticfiles', 19 | 20 | 'django_extensions', 21 | 'ckeditor', 22 | 'post_office', 23 | 24 | 'course_interviews' 25 | ) 26 | 27 | EMAIL_BACKEND = 'post_office.EmailBackend' 28 | POST_OFFICE = { 29 | 'LOG_LEVEL': 2 # logs everything (both successful and failed delivery attempts) 30 | } 31 | 32 | # End of Post_Office settings 33 | 34 | MIDDLEWARE_CLASSES = ( 35 | 'django.contrib.sessions.middleware.SessionMiddleware', 36 | 'django.middleware.common.CommonMiddleware', 37 | 'django.middleware.csrf.CsrfViewMiddleware', 38 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 39 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 42 | 'django.middleware.security.SecurityMiddleware', 43 | ) 44 | 45 | ROOT_URLCONF = 'interview_communicator.urls' 46 | 47 | TEMPLATES = [ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [], 51 | 'APP_DIRS': True, 52 | 'OPTIONS': { 53 | 'context_processors': [ 54 | 'django.template.context_processors.debug', 55 | 'django.template.context_processors.request', 56 | 'django.contrib.auth.context_processors.auth', 57 | 'django.contrib.messages.context_processors.messages', 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | WSGI_APPLICATION = 'interview_communicator.wsgi.application' 64 | 65 | # Internationalization 66 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 67 | 68 | LANGUAGE_CODE = 'en-us' 69 | 70 | TIME_ZONE = 'UTC' 71 | 72 | USE_I18N = True 73 | 74 | USE_L10N = True 75 | 76 | USE_TZ = True 77 | 78 | 79 | TEMPLATE_CONTEXT_PROCESSORS = TCP + ( 80 | 'django.core.context_processors.request', 81 | ) 82 | 83 | SUIT_CONFIG = { 84 | # header 85 | 'ADMIN_NAME': 'Admin', 86 | 'HEADER_DATE_FORMAT': 'l, j. F Y', 87 | 'HEADER_TIME_FORMAT': 'H:i', 88 | 89 | # forms 90 | 'SHOW_REQUIRED_ASTERISK': True, # Default True 91 | 'CONFIRM_UNSAVED_CHANGES': True, # Default True 92 | 93 | # menu 94 | # 'SEARCH_URL': '/admin/hack_fmi/competitor/', 95 | 'MENU_ICONS': { 96 | 'auth': 'icon-lock', 97 | }, 98 | 'MENU_OPEN_FIRST_CHILD': True, # Default True 99 | 'MENU_EXCLUDE': ('auth.group',), 100 | 'MENU': ( 101 | {'app': 'course_interviews', 'icon': 'icon-pencil'}, 102 | {'app': 'post_office', 'icon': 'icon-envelope'}, 103 | ), 104 | 105 | # misc 106 | 'LIST_PER_PAGE': 100 107 | } 108 | 109 | AUTH_USER_MODEL = 'course_interviews.Teacher' 110 | 111 | try: 112 | from .local_settings import * 113 | except ImportError: 114 | exit("local_settings.py not found!") 115 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/generate_students.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student 2 | from .get_students import AbstractGetStudents 3 | from .applicant import Applicant 4 | 5 | 6 | class GenerateStudents(AbstractGetStudents): 7 | 8 | def __init__(self, address, form_name, api_key, count, page): 9 | super().__init__(address, form_name, api_key, count, page) 10 | self.__students_with_finalized_applications = 0 11 | self.__errors = 0 12 | 13 | def __inc_errors(self): 14 | self.__errors += 1 15 | 16 | def __inc_students_with_finalized_applications(self): 17 | self.__students_with_finalized_applications += 1 18 | 19 | def __add_applicant(self, person): 20 | if person["status"] == "Finalized": 21 | applicant = Applicant( 22 | name=person["questions"][0]["question_response"], 23 | studies_at=person["questions"][1]["question_response"], 24 | works_at=person["questions"][2]["question_response"], 25 | first_task=person["questions"][3]["question_response"], 26 | second_task=person["questions"][4]["question_response"], 27 | third_task=person["questions"][5]["question_response"], 28 | applied_course=person["questions"][6]["field_response"][0], 29 | email=person["questions"][7]["question_response"], 30 | skype=person["questions"][8]["question_response"], 31 | phone_number=person["questions"][9]["question_response"], 32 | ) 33 | student = Student( 34 | name=applicant.get_name(), 35 | studies_at=applicant.get_studies_at(), 36 | works_at=applicant.get_works_at(), 37 | first_task=applicant.get_first_task(), 38 | second_task=applicant.get_second_task(), 39 | third_task=applicant.get_third_task(), 40 | applied_course=applicant.get_applied_course(), 41 | email=applicant.get_email(), 42 | skype=applicant.get_skype(), 43 | phone_number=applicant.get_phone_number(), 44 | ) 45 | try: 46 | self.__inc_students_with_finalized_applications() 47 | student.save() 48 | except Exception as e: 49 | self.__inc_errors() 50 | # the regular exception when saving student (that is already in the base) in the 51 | # base is email already taken - this error should be seen when 52 | # manage.py generate_students is invoked more than once 53 | # Error is not handled because of the different DB types 54 | print(e) 55 | pass 56 | 57 | def generate_students(self): 58 | while (True): 59 | applications = self.get_student_applications() 60 | 61 | # Break loop if all students are added 62 | if applications["items_count"] == 0: 63 | break 64 | 65 | self.page += 1 66 | for person in applications["data"]: 67 | self.__add_applicant(person) 68 | 69 | def get_errors(self): 70 | return self.__errors 71 | 72 | def get_generated_students(self): 73 | return self.__students_with_finalized_applications - self.__errors 74 | 75 | def get_students_with_finalized_applications(self): 76 | return self.__students_with_finalized_applications 77 | 78 | def get_students_in_base(self): 79 | return len(Student.objects.all()) 80 | -------------------------------------------------------------------------------- /source/course_interviews/helpers/generate_emails.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student 2 | from post_office import mail 3 | 4 | 5 | class GenerateNewCoursesEmails: 6 | 7 | def __init__(self, template): 8 | self.template = template 9 | self.generated_emails = 0 10 | self.errors = 0 11 | 12 | def inc_errors(self): 13 | self.errors += 1 14 | 15 | def inc_generated_emails(self): 16 | self.generated_emails += 1 17 | 18 | def generate_new_courses_emails(self): 19 | # Emails are gona be send to all applicants that applied successfully 20 | recipients = [student for student in Student.objects.filter( 21 | has_received_new_courses_email=False)] 22 | 23 | for student in recipients: 24 | mail.send( 25 | recipients=[student.email], 26 | template=self.template, 27 | context={ 28 | 'name': student.name, 29 | }) 30 | try: 31 | self.inc_generated_emails() 32 | student.has_received_new_courses_email = True 33 | student.save() 34 | except Exception as e: 35 | self.inc_errors() 36 | student.has_received_new_courses_email = False 37 | student.save() 38 | print(e) 39 | pass 40 | 41 | def get_generated_emails(self): 42 | return self.generated_emails - self.errors 43 | 44 | def get_errors(self): 45 | return self.errors 46 | 47 | def get_students_with_generated_emails(self): 48 | return len(Student.objects.filter(has_received_new_courses_email=True)) 49 | 50 | def get_students_without_generated_emails(self): 51 | return len(Student.objects.filter(has_received_new_courses_email=False)) 52 | 53 | 54 | class GenerateConfirmEmails(GenerateNewCoursesEmails): 55 | 56 | def __init__(self, template, confirm_interview_url, choose_interview_url): 57 | super().__init__(template) 58 | self.confirm_interview_url = confirm_interview_url 59 | self.choose_interview_url = choose_interview_url 60 | 61 | def generate_confirmation_emails(self): 62 | # Emails are gona be send to all applicants that have 63 | # interview date and didn't receive an email yet 64 | recipients = [student for student in Student.objects.filter( 65 | has_received_email=False, 66 | has_interview_date=True)] 67 | 68 | for student in recipients: 69 | mail.send( 70 | recipients=[student.email], 71 | template=self.template, 72 | context={ 73 | 'name': student.name, 74 | 'applied_course': student.applied_course, 75 | 'skype': student.interviewslot.teacher_time_slot.teacher.skype, 76 | 'interview_date': student.interviewslot.teacher_time_slot.date, 77 | 'interview_start_time': student.interviewslot.start_time, 78 | 'confirm_interview_url': self.confirm_interview_url + student.uuid, 79 | 'choose_interview_url': self.choose_interview_url + student.uuid, 80 | }) 81 | self.inc_generated_emails() 82 | student.has_received_email = True 83 | student.save() 84 | 85 | def get_generated_emails(self): 86 | return self.generated_emails - self.errors 87 | 88 | def get_errors(self): 89 | return self.errors 90 | 91 | def get_students_with_generated_emails(self): 92 | return len(Student.objects.filter(has_received_email=True)) 93 | 94 | def get_students_without_generated_emails(self): 95 | return len(Student.objects.filter(has_received_email=False)) 96 | -------------------------------------------------------------------------------- /source/course_interviews/templates/choose_interview.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block 'page-content' %} 4 | {% autoescape off %} 5 |
6 |
7 |
8 | 9 |
10 |

Choose your interview date and hour

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
DateStarting time
24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | {% endautoescape %} 32 | 33 | 34 | 52 | 53 | 54 | 73 | 74 | 75 | 84 | 85 | {% endblock %} 86 | {% block 'scripts' %} 87 | 118 | {% endblock %} -------------------------------------------------------------------------------- /source/course_interviews/apis.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404 2 | from django.http import JsonResponse, HttpResponse, HttpResponseNotFound 3 | from django.contrib.admin.views.decorators import staff_member_required 4 | from .models import Student, InterviewSlot 5 | from .helpers.course_students import CourseStudents 6 | from .helpers.get_students_emails import GetStudentsEmails, GetAllStudentsEmails 7 | from .helpers.get_free_interview_slots import get_free_interview_slots 8 | from interview_communicator.local_settings import (f6s_address, f6s_application_name, f6s_api_key, 9 | f6s_page_count, f6s_page) 10 | 11 | 12 | # Function providing the students that applied for a specific course and submited their applications 13 | def get_students(request, course): 14 | if course == "Csharp": 15 | course = "Programming 101 with C#" 16 | elif course == "Java": 17 | course = "Programming 101 with Java" 18 | elif course == "Ruby": 19 | course = "Programming 101 with Ruby" 20 | elif course == "Python": 21 | course = "Programming 101 with Python" 22 | 23 | course_students_generator = CourseStudents( 24 | f6s_address, f6s_application_name, f6s_api_key, f6s_page_count, f6s_page, course) 25 | 26 | course_students_generator.generate_students_for_course() 27 | json = course_students_generator.get_json() 28 | 29 | return JsonResponse(json) 30 | 31 | 32 | # Function for the emails of all applicants that left email in their F6S application, 33 | # but did not yet submit their tasks - we are gona send them a reminder email for submition 34 | @staff_member_required 35 | def get_emails(request): 36 | courses = [ 37 | "Programming 101 with C#", 38 | "Programming 101 with Java", 39 | "Programming 101 with Ruby", 40 | "Programming 101 with Python" 41 | ] 42 | 43 | get_students_emails_generator = GetStudentsEmails( 44 | f6s_address, f6s_application_name, f6s_api_key, f6s_page_count, f6s_page, courses) 45 | 46 | get_students_emails_generator.generate_students_emails() 47 | json = get_students_emails_generator.get_json() 48 | 49 | return JsonResponse(json) 50 | 51 | 52 | # Function for all of the emails given in the F6S form 53 | @staff_member_required 54 | def get_all_emails(request): 55 | courses = [] # Get all courses 56 | 57 | get_students_emails_generator = GetAllStudentsEmails( 58 | f6s_address, f6s_application_name, f6s_api_key, f6s_page_count, f6s_page, courses) 59 | 60 | get_students_emails_generator.generate_students_emails() 61 | json = get_students_emails_generator.get_json() 62 | 63 | return JsonResponse((", ").join(json["students"]), safe=False) 64 | 65 | 66 | # Function serving interview slots to HandleBars 67 | # TODO: use DRF 68 | def get_interview_slots(request): 69 | json = [] 70 | available_slots = get_free_interview_slots() 71 | 72 | for slot in available_slots: 73 | json.append({ 74 | "date": slot.teacher_time_slot.date, 75 | "time": slot.start_time, 76 | "slot_id": slot.id 77 | }) 78 | 79 | return JsonResponse(json, safe=False) 80 | 81 | 82 | def confirm_student_interview(request): 83 | if request.POST: 84 | token = request.POST["token"] 85 | student = get_object_or_404(Student, uuid=token) 86 | 87 | if not student.has_interview_date: 88 | raise HttpResponseNotFound( 89 | "Student does not have an interview date") 90 | 91 | student.has_confirmed_interview = True 92 | student.save() 93 | return HttpResponse("OK") 94 | 95 | 96 | def confirm_slot(request): 97 | if request.POST: 98 | slot_id = request.POST["slot_id"] 99 | student_uuid = request.POST["student_uuid"] 100 | 101 | slot = get_object_or_404(InterviewSlot, id=slot_id) 102 | student = get_object_or_404(Student, uuid=student_uuid) 103 | 104 | if slot.student: 105 | return HttpResponseNotFound( 106 | "This interview slot is already taken! Please select another one") 107 | 108 | if student.has_confirmed_interview: 109 | return HttpResponseNotFound( 110 | "You already confirmed your interview. You can't choose another one.") 111 | 112 | # The auto generated slot student already has should become free 113 | try: 114 | vacate_slot = InterviewSlot.objects.get(student=student) 115 | vacate_slot.student = None 116 | vacate_slot.save() 117 | except: 118 | pass 119 | 120 | slot.student = student 121 | student.has_interview_date = True 122 | student.has_confirmed_interview = True 123 | 124 | slot.save() 125 | student.save() 126 | 127 | return HttpResponse("OK") 128 | -------------------------------------------------------------------------------- /source/course_interviews/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.conf import settings 6 | import phonenumber_field.modelfields 7 | import django_extensions.db.fields 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Teacher', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), 21 | ('password', models.CharField(verbose_name='password', max_length=128)), 22 | ('last_login', models.DateTimeField(blank=True, verbose_name='last login', null=True)), 23 | ('email', models.EmailField(unique=True, max_length=254)), 24 | ('is_active', models.BooleanField(default=True)), 25 | ('is_admin', models.BooleanField(default=False)), 26 | ('is_staff', models.BooleanField(default=False)), 27 | ('skype', models.CharField(help_text='Enter the skype of the teacher!', default=None, max_length=50)), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='InterviewerFreeTime', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), 37 | ('date', models.DateField(null=True)), 38 | ('start_time', models.TimeField(null=True)), 39 | ('end_time', models.TimeField(null=True)), 40 | ('teacher', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='InterviewSlot', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), 47 | ('start_time', models.TimeField(null=True)), 48 | ], 49 | ), 50 | migrations.CreateModel( 51 | name='Student', 52 | fields=[ 53 | ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), 54 | ('name', models.CharField(max_length=50)), 55 | ('email', models.EmailField(unique=True, max_length=254)), 56 | ('skype', models.CharField(default=None, max_length=110)), 57 | ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128)), 58 | ('applied_course', models.CharField(blank=True, max_length=110, null=True)), 59 | ('first_task', models.URLField(blank=True, null=True)), 60 | ('second_task', models.URLField(blank=True, null=True)), 61 | ('third_task', models.URLField(blank=True, null=True)), 62 | ('studies_at', models.CharField(blank=True, max_length=110, null=True)), 63 | ('works_at', models.CharField(blank=True, max_length=110, null=True)), 64 | ('code_skills_rating', models.IntegerField(help_text='Оценка върху уменията на кандидата да пише код и знанията му върху базови алгоритми', default=0, choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10)])), 65 | ('code_design_rating', models.IntegerField(help_text='Оценка върху уменията на кандидата да "съставя програми" и да разбива нещата по парчета + базово OOP', default=0, choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10)])), 66 | ('fit_attitude_rating', models.IntegerField(help_text='Оценка на интервюиращия в зависимост от усета му за човека (става ли за курса)', default=0, choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10)])), 67 | ('teacher_comment', models.TextField(help_text='Коментар на интервюиращия за цялостното представяне на кандидата', blank=True, null=True)), 68 | ('has_interview_date', models.BooleanField(default=False)), 69 | ('has_received_email', models.BooleanField(default=False)), 70 | ('has_confirmed_interview', models.BooleanField(default=False)), 71 | ('has_been_interviewed', models.BooleanField(default=False)), 72 | ('is_accepted', models.BooleanField(default=False)), 73 | ('uuid', django_extensions.db.fields.UUIDField(blank=True, default=uuid.uuid4, editable=False, unique=True)), 74 | ], 75 | ), 76 | migrations.AddField( 77 | model_name='interviewslot', 78 | name='student', 79 | field=models.OneToOneField(to='course_interviews.Student', null=True), 80 | ), 81 | migrations.AddField( 82 | model_name='interviewslot', 83 | name='teacher_time_slot', 84 | field=models.ForeignKey(to='course_interviews.InterviewerFreeTime'), 85 | ), 86 | ] 87 | -------------------------------------------------------------------------------- /source/course_interviews/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | from phonenumber_field.modelfields import PhoneNumberField 4 | from django_extensions.db.fields import UUIDField 5 | from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin 6 | import uuid 7 | 8 | 9 | class SiteUserManager(BaseUserManager): 10 | 11 | def create_user(self, email, password=None, **extra_fields): 12 | if not email: 13 | raise ValueError('The given email address must be set') 14 | 15 | email = SiteUserManager.normalize_email(email) 16 | user = self.model(email=email, 17 | is_staff=False, is_active=True, **extra_fields) 18 | 19 | user.set_password(password) 20 | user.save(using=self._db) 21 | return user 22 | 23 | def create_superuser(self, email, password, **extra_fields): 24 | u = self.create_user(email, password, **extra_fields) 25 | u.is_staff = True 26 | u.is_active = True 27 | u.is_superuser = True 28 | u.save(using=self._db) 29 | return u 30 | 31 | 32 | class Teacher(AbstractBaseUser, PermissionsMixin): 33 | 34 | objects = SiteUserManager() 35 | 36 | email = models.EmailField(unique=True, blank=False) 37 | skype = models.CharField( 38 | default=None, 39 | max_length=50, 40 | help_text='Enter the skype of the teacher!') 41 | first_name = models.CharField(default="", max_length=50) 42 | last_name = models.CharField(default="", max_length=50) 43 | 44 | is_active = models.BooleanField(default=True) 45 | is_admin = models.BooleanField(default=False) 46 | is_staff = models.BooleanField(default=False) 47 | 48 | USERNAME_FIELD = 'email' 49 | REQUIRED_FIELDS = ['first_name', 'last_name', 'skype'] 50 | 51 | def get_full_name(self): 52 | return self.first_name + " " + self.last_name 53 | 54 | def get_short_name(self): 55 | return self.first_name 56 | 57 | def __str__(self): 58 | return self.get_full_name() 59 | 60 | 61 | class Student(models.Model): 62 | name = models.CharField(max_length=50) 63 | email = models.EmailField(unique=True) 64 | skype = models.CharField(default=None, max_length=1100) 65 | phone_number = PhoneNumberField(blank=True) 66 | applied_course = models.CharField(null=True, blank=True, max_length=1100) 67 | first_task = models.URLField(null=True, blank=True) 68 | second_task = models.URLField(null=True, blank=True) 69 | third_task = models.URLField(null=True, blank=True) 70 | studies_at = models.CharField(blank=True, null=True, max_length=1100) 71 | works_at = models.CharField(null=True, blank=True, max_length=1100) 72 | 73 | # possible_rating is number between 1 and 10 to be selected in the integer field 74 | possible_ratings = [(i, i) for i in range(11)] 75 | code_skills_rating = models.IntegerField( 76 | default=0, 77 | choices=possible_ratings, 78 | help_text='Оценка върху уменията на кандидата да пише' 79 | ' код и знанията му върху базови алгоритми') 80 | code_design_rating = models.IntegerField( 81 | default=0, 82 | choices=possible_ratings, 83 | help_text='Оценка върху уменията на кандидата да "съставя' 84 | ' програми" и да разбива нещата по парчета + базово OOP') 85 | fit_attitude_rating = models.IntegerField( 86 | default=0, 87 | choices=possible_ratings, 88 | help_text='Оценка на интервюиращия в зависимост от' 89 | ' усета му за човека (става ли за курса)') 90 | teacher_comment = models.TextField( 91 | null=True, 92 | blank=True, 93 | help_text='Коментар на интервюиращия за цялостното представяне на кандидата') 94 | has_interview_date = models.BooleanField(default=False) 95 | has_received_email = models.BooleanField(default=False) 96 | has_received_new_courses_email = models.BooleanField(default=False) 97 | has_confirmed_interview = models.BooleanField(default=False) 98 | has_been_interviewed = models.BooleanField(default=False) 99 | is_accepted = models.BooleanField(default=False) 100 | uuid = UUIDField(version=4, unique=True, default=uuid.uuid4) 101 | 102 | def __str__(self): 103 | return self.name 104 | 105 | 106 | class InterviewerFreeTime(models.Model): 107 | teacher = models.ForeignKey(Teacher) 108 | date = models.DateField(blank=False, null=True) 109 | start_time = models.TimeField(blank=False, null=True) 110 | end_time = models.TimeField(blank=False, null=True) 111 | buffer_time = models.BooleanField(default=False) 112 | 113 | # method for checking if the slots for the specific interviewer free time 114 | # are already generated. Method is called by /helpers/generate_interview_slots.py 115 | def has_generated_slots(self): 116 | return self.interviewslot_set.exists() 117 | 118 | def __str__(self): 119 | return str(self.date) + " - from " + str(self.start_time) + " to " + str(self.end_time) 120 | 121 | 122 | class InterviewSlot(models.Model): 123 | teacher_time_slot = models.ForeignKey(InterviewerFreeTime) 124 | student = models.OneToOneField(Student, null=True, blank=True) 125 | start_time = models.TimeField(blank=False, null=True) 126 | buffer_slot = models.BooleanField(default=False) 127 | -------------------------------------------------------------------------------- /source/course_interviews/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student, Teacher, InterviewerFreeTime, InterviewSlot 2 | from django.contrib.auth.models import Group, Permission 3 | from django.core.urlresolvers import reverse 4 | from django.test import TestCase 5 | from datetime import date, timedelta 6 | 7 | 8 | class ViewsTests(TestCase): 9 | 10 | def setUp(self): 11 | self.teacher_admin = Teacher.objects.create_superuser( 12 | "admin@admin.com", "123", skype="admin_hackbulgaria") 13 | 14 | self.teacher_user1 = Teacher.objects.create_user( 15 | "user1@user.com", "123", skype="user1_user") 16 | 17 | self.teacher_user2 = Teacher.objects.create_user( 18 | "user2@user.com", "123", skype="user2_user") 19 | 20 | teacher_user_permission_names = [ 21 | 'add_interviewerfreetime', 22 | 'change_interviewerfreetime', 23 | 'delete_interviewerfreetime', 24 | 'change_interviewslot', 25 | 'add_student', 26 | 'change_student', 27 | ] 28 | 29 | teacher_user_permissions = Permission.objects.filter( 30 | codename__in=teacher_user_permission_names 31 | ) 32 | 33 | self.teacher_group = Group.objects.create(name='Editor') 34 | self.teacher_group.permissions = teacher_user_permissions 35 | self.teacher_group.save() 36 | 37 | self.teacher_user1.first_name = "Ivo" 38 | self.teacher_user1.last_name = "Radov" 39 | self.teacher_user1.is_staff = True 40 | self.teacher_user1.groups.add(self.teacher_group) 41 | self.teacher_user1.save() 42 | 43 | self.teacher_user2.first_name = "Rado" 44 | self.teacher_user2.last_name = "Ivov" 45 | self.teacher_user2.is_staff = True 46 | self.teacher_user2.groups.add(self.teacher_group) 47 | self.teacher_user2.save() 48 | 49 | self.tomorrow = date.today() + timedelta(days=1) 50 | 51 | self.teacher_free_time1 = InterviewerFreeTime.objects.create( 52 | teacher=self.teacher_user1, 53 | date=str(self.tomorrow), 54 | start_time="15:00", 55 | end_time="16:00") 56 | 57 | self.teacher_free_time2 = InterviewerFreeTime.objects.create( 58 | teacher=self.teacher_user2, 59 | date=str(self.tomorrow), 60 | start_time="16:00", 61 | end_time="17:00") 62 | 63 | self.interview_slot1 = InterviewSlot.objects.create( 64 | teacher_time_slot=self.teacher_free_time1, 65 | start_time="15:00") 66 | 67 | self.interview_slot2 = InterviewSlot.objects.create( 68 | teacher_time_slot=self.teacher_free_time2, 69 | start_time="16:00") 70 | 71 | self.student1 = Student.objects.create( 72 | name="Student One", 73 | email="student1@student.com", 74 | skype="student_one_skype") 75 | 76 | self.student2 = Student.objects.create( 77 | name="Student Two", 78 | email="student2@student.com", 79 | skype="student_two_skype") 80 | 81 | def test_show_index(self): 82 | url = reverse('course_interviews:index') 83 | response = self.client.get(url, follow=True) 84 | 85 | self.assertEqual(response.status_code, 200) 86 | 87 | def test_choose_interview_for_student_with_confirmed_interview(self): 88 | """ 89 | Student with confirmed interview should not be able to select another interview 90 | """ 91 | self.student1.has_confirmed_interview = True 92 | self.student1.save() 93 | url = reverse('course_interviews:choose_interview', args=(self.student1.uuid, )) 94 | response = self.client.get(url, follow=True) 95 | 96 | self.assertEqual(response.status_code, 404) 97 | 98 | def test_choose_interview_slot(self): 99 | """ 100 | After choosing new interview, student.has_interview_date should be True 101 | Student in the interview slot should be the right one 102 | """ 103 | url = reverse('course_interviews:confirm_slot') 104 | data = { 105 | "slot_id": self.interview_slot1.id, 106 | "student_uuid": self.student1.uuid 107 | } 108 | response = self.client.post(url, data, follow=True) 109 | 110 | self.assertEqual(response.status_code, 200) 111 | self.assertEqual(Student.objects.get(id=self.student1.id).has_interview_date, True) 112 | self.assertEqual( 113 | InterviewSlot.objects.get(id=self.interview_slot1.id).student, self.student1) 114 | 115 | def test_choose_interview_slot_if_slot_is_already_taken(self): 116 | """ 117 | Student should't be able to choose a slot that's already taken 118 | Slots are refreshed every 10 seconds 119 | If already selected slot is chosen, the student will be promped to choose another interview 120 | """ 121 | self.interview_slot1.student = self.student2 122 | self.interview_slot1.save() 123 | url = reverse('course_interviews:confirm_slot') 124 | data = { 125 | "slot_id": self.interview_slot1.id, 126 | "student_uuid": self.student1.uuid 127 | } 128 | response = self.client.post(url, data, follow=True) 129 | 130 | self.assertEqual(response.status_code, 404) 131 | 132 | def test_choose_interview_for_student_with_interview_date(self): 133 | """ 134 | If the student didn't confirm his interview, he can choose another one 135 | If he chooses free slot, his previous (autogenerated) slot should become free 136 | """ 137 | self.interview_slot1.student = self.student1 138 | self.interview_slot1.save() 139 | self.student1.has_interview_date = True 140 | self.student1.save() 141 | url = reverse('course_interviews:confirm_slot') 142 | data = { 143 | "slot_id": self.interview_slot2.id, 144 | "student_uuid": self.student1.uuid 145 | } 146 | self.client.post(url, data, follow=True) 147 | 148 | self.assertEqual( 149 | InterviewSlot.objects.get(id=self.interview_slot1.id).student, None) 150 | self.assertEqual( 151 | InterviewSlot.objects.get(id=self.interview_slot2.id).student, self.student1) 152 | -------------------------------------------------------------------------------- /source/course_interviews/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student, Teacher, InterviewerFreeTime, InterviewSlot 2 | from course_interviews.helpers.generate_interview_slots import GenerateInterviewSlots 3 | from django.contrib.auth.models import Group, Permission 4 | from django.core.urlresolvers import reverse 5 | from django.test import TestCase, Client 6 | from datetime import date, timedelta 7 | 8 | 9 | class AdminPanelTests(TestCase): 10 | 11 | def setUp(self): 12 | self.teacher_admin = Teacher.objects.create_superuser( 13 | "admin@admin.com", "123", skype="admin_hackbulgaria") 14 | 15 | self.teacher_user1 = Teacher.objects.create_user( 16 | "user1@user.com", "123", skype="user1_user") 17 | 18 | teacher_user_permission_names = [ 19 | 'add_interviewerfreetime', 20 | 'change_interviewerfreetime', 21 | 'delete_interviewerfreetime', 22 | 'change_interviewslot', 23 | 'add_student', 24 | 'change_student', 25 | ] 26 | 27 | teacher_user_permissions = Permission.objects.filter( 28 | codename__in=teacher_user_permission_names 29 | ) 30 | 31 | self.teacher_group = Group.objects.create(name='Editor') 32 | self.teacher_group.permissions = teacher_user_permissions 33 | self.teacher_group.save() 34 | 35 | self.teacher_user1.first_name = "Ivo" 36 | self.teacher_user1.last_name = "Radov" 37 | self.teacher_user1.is_staff = True 38 | self.teacher_user1.groups.add(self.teacher_group) 39 | self.teacher_user1.save() 40 | 41 | self.tomorrow = date.today() + timedelta(days=1) 42 | 43 | self.teacher_free_time1 = InterviewerFreeTime.objects.create( 44 | teacher=self.teacher_user1, 45 | date=str(self.tomorrow), 46 | start_time="15:00", 47 | end_time="16:00") 48 | 49 | self.interview_slot1 = InterviewSlot.objects.create( 50 | teacher_time_slot=self.teacher_free_time1, 51 | start_time="15:00") 52 | 53 | self.student1 = Student.objects.create( 54 | name="Student One", 55 | email="student1@student.com", 56 | skype="student_one_skype") 57 | 58 | def test_interviewer_free_time_admin_queryset_for_teacher_user(self): 59 | """ 60 | teacher_user should see only his interview free times 61 | """ 62 | client = Client() 63 | client.login( 64 | email=self.teacher_user1.email, 65 | password='123' 66 | ) 67 | url = reverse('admin:course_interviews_interviewerfreetime_changelist') 68 | response = client.get(url, follow=True) 69 | result_list = response.context_data['cl'].result_list 70 | change_list = self.teacher_user1.interviewerfreetime_set.all() 71 | 72 | self.assertCountEqual(change_list, result_list) 73 | 74 | def test_interviewer_free_time_admin_save_model_on_create(self): 75 | """ 76 | save_model should put teacher_user on create object 77 | """ 78 | client = Client() 79 | client.login( 80 | email=self.teacher_user1.email, 81 | password='123' 82 | ) 83 | 84 | data = { 85 | 'date': str(self.tomorrow), 86 | 'start_time': '18:00', 87 | 'end_time': '19:00' 88 | } 89 | url = reverse('admin:course_interviews_interviewerfreetime_add') 90 | client.post(url, data, follow=True) 91 | new_teacher_free_time = InterviewerFreeTime.objects.latest('id') 92 | 93 | self.assertEqual(new_teacher_free_time.teacher, self.teacher_user1) 94 | 95 | def test_interview_slot_admin_change_for_teacher_user(self): 96 | """ 97 | teacher_user should be able to see the slots (has change permissions), 98 | but should not be able to edit the slots (Forbidden - Error 403) 99 | """ 100 | client = Client() 101 | client.login( 102 | email=self.teacher_user1.email, 103 | password='123' 104 | ) 105 | url = reverse( 106 | 'admin:course_interviews_interviewslot_change', 107 | args=(self.interview_slot1.id, )) 108 | data = { 109 | "student": self.student1 110 | } 111 | response = client.post(url, data, follow=True) 112 | 113 | self.assertEqual(response.status_code, 403) 114 | 115 | def test_interview_slot_admin_queryset_for_teacher_user(self): 116 | """ 117 | teacher_user should see only his interview slots 118 | """ 119 | client = Client() 120 | client.login( 121 | email=self.teacher_user1.email, 122 | password='123' 123 | ) 124 | url = reverse('admin:course_interviews_interviewslot_changelist') 125 | response = client.get(url, follow=True) 126 | result_list = response.context_data['cl'].result_list 127 | change_list = InterviewSlot.objects.all().filter(teacher_time_slot=self.teacher_free_time1) 128 | 129 | self.assertCountEqual(change_list, result_list) 130 | 131 | def test_change_permissions_interviewer_free_time_after_slots_are_generated(self): 132 | """ 133 | teacher should not be able to delete his interview_free_time if slots 134 | for that time are already generated - no interviews are gona be unanswered 135 | if he can not attend some interview, he has to tell us 136 | """ 137 | test_teacher_user = Teacher.objects.create_user( 138 | "testuser@user.com", "123", skype="testuser_user") 139 | 140 | test_teacher_user.first_name = "Test" 141 | test_teacher_user.last_name = "Testov" 142 | test_teacher_user.is_staff = True 143 | test_teacher_user.groups.add(self.teacher_group) 144 | test_teacher_user.save() 145 | 146 | interview_length = 20 147 | break_between_interviews = 10 148 | interview_slots_generator = GenerateInterviewSlots( 149 | interview_length, break_between_interviews) 150 | 151 | time_slot = InterviewerFreeTime.objects.create( 152 | teacher=test_teacher_user, 153 | date=str(self.tomorrow), 154 | start_time="15:00", 155 | end_time="15:30") 156 | 157 | interview_slots_generator.generate_interview_slots() 158 | 159 | client = Client() 160 | client.login( 161 | email=test_teacher_user.email, 162 | password='123' 163 | ) 164 | 165 | url = reverse('admin:course_interviews_interviewerfreetime_change', 166 | args=(time_slot.id, )) 167 | 168 | data = { 169 | "buffer_time": True 170 | } 171 | 172 | response = client.post(url, data, follow=True) 173 | self.assertEqual(response.status_code, 403) 174 | -------------------------------------------------------------------------------- /source/course_interviews/admin.py: -------------------------------------------------------------------------------- 1 | from .helpers.generate_interview_slots import GenerateInterviewSlots 2 | from .helpers.generate_interviews import GenerateInterviews 3 | from .helpers.generate_emails import GenerateConfirmEmails 4 | from interview_communicator.local_settings import confirm_interview_url, choose_interview_url 5 | from django.contrib import admin 6 | from django.contrib.auth.admin import UserAdmin 7 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 8 | from django import forms 9 | from django.http import HttpResponseRedirect, HttpResponseForbidden 10 | from django.conf.urls import patterns, url 11 | from .models import Student, Teacher, InterviewerFreeTime, InterviewSlot 12 | 13 | 14 | class UserCreationForm(forms.ModelForm): 15 | 16 | """A form for creating new users. Includes all the required 17 | fields, plus a repeated password.""" 18 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput) 19 | password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) 20 | 21 | class Meta: 22 | model = Teacher 23 | fields = ('email',) 24 | 25 | def clean_password2(self): 26 | password1 = self.cleaned_data.get("password1") 27 | password2 = self.cleaned_data.get("password2") 28 | if password1 and password2 and password1 != password2: 29 | raise forms.ValidationError("Passwords don't match") 30 | return password2 31 | 32 | def save(self, commit=True): 33 | user = super(UserCreationForm, self).save(commit=False) 34 | user.set_password(self.cleaned_data["password1"]) 35 | if commit: 36 | user.save() 37 | return user 38 | 39 | 40 | class UserChangeForm(forms.ModelForm): 41 | 42 | """A form for updating users. Includes all the fields on 43 | the user, but replaces the password field with admin's 44 | password hash display field. 45 | """ 46 | password = ReadOnlyPasswordHashField() 47 | 48 | class Meta: 49 | model = Teacher 50 | fields = ('email', 'password', 'is_active', 'is_admin') 51 | 52 | def clean_password(self): 53 | return self.initial["password"] 54 | 55 | 56 | class MyUserAdmin(UserAdmin): 57 | form = UserChangeForm 58 | add_form = UserCreationForm 59 | 60 | list_display = ('get_full_name', 'email', 'skype') 61 | list_filter = ('is_admin',) 62 | 63 | ordering = None 64 | 65 | fieldsets = ( 66 | (None, { 67 | 'fields': ( 68 | 'email', 'password', 'first_name', 'last_name', 'skype', 69 | 'groups', 'is_active', 'is_staff', 'is_superuser') 70 | }), 71 | ('Permissions', {'fields': ('is_admin',)}), 72 | ) 73 | 74 | add_fieldsets = ( 75 | (None, { 76 | 'classes': ('wide',), 77 | 'fields': ('email', 'skype', 'password1', 'password2') 78 | }), 79 | ) 80 | 81 | admin.site.register(Teacher, MyUserAdmin) 82 | 83 | 84 | class StudentAdmin(admin.ModelAdmin): 85 | 86 | def get_first_task(self, obj): 87 | return u"
link".format(obj.first_task) 88 | get_first_task.allow_tags = True 89 | get_first_task.short_description = "First task" 90 | get_first_task.admin_order_field = 'first_task' 91 | 92 | def get_second_task(self, obj): 93 | return u"link".format(obj.second_task) 94 | get_second_task.allow_tags = True 95 | get_second_task.short_description = "Second task" 96 | get_second_task.admin_order_field = 'second_task' 97 | 98 | def get_third_task(self, obj): 99 | return u"link".format(obj.third_task) 100 | get_third_task.allow_tags = True 101 | get_third_task.short_description = "Third task" 102 | get_third_task.admin_order_field = 'third_task' 103 | 104 | list_display = [ 105 | 'name', 106 | 'email', 107 | 'skype', 108 | 'phone_number', 109 | 'applied_course', 110 | 'get_first_task', 111 | 'get_second_task', 112 | 'get_third_task', 113 | 'code_skills_rating', 114 | 'code_design_rating', 115 | 'fit_attitude_rating', 116 | 'has_interview_date', 117 | 'has_confirmed_interview', 118 | 'has_been_interviewed', 119 | 'is_accepted' 120 | ] 121 | list_filter = [ 122 | 'applied_course', 123 | 'code_skills_rating', 124 | 'code_design_rating', 125 | 'fit_attitude_rating', 126 | 'has_confirmed_interview', 127 | 'has_been_interviewed', 128 | 'is_accepted' 129 | ] 130 | search_fields = ['name', 'email', 'skype'] 131 | readonly_fields = ('uuid',) 132 | 133 | admin.site.register(Student, StudentAdmin) 134 | 135 | 136 | class InterviewerFreeTimeAdmin(admin.ModelAdmin): 137 | model = InterviewerFreeTime 138 | 139 | def has_change_permission(self, request, obj=None): 140 | if obj and request.POST and not request.user.is_superuser and obj.has_generated_slots(): 141 | return False 142 | return super().has_change_permission(request, obj) 143 | 144 | def get_form(self, request, obj=None, **kwargs): 145 | self.exclude = [] 146 | if not request.user.is_superuser: 147 | self.exclude = ['teacher', 'buffer_time'] 148 | return super().get_form(request, obj, **kwargs) 149 | 150 | def save_model(self, request, obj, form, change): 151 | if not change and not request.user.is_superuser: 152 | obj.teacher = request.user 153 | obj.save() 154 | 155 | def get_queryset(self, request): 156 | queryset = super().get_queryset(request) 157 | if request.user.is_superuser: 158 | return queryset 159 | return queryset.filter(teacher=request.user) 160 | 161 | def get_generated_slots(self, obj): 162 | return obj.has_generated_slots() 163 | get_generated_slots.short_description = "Has generated slots" 164 | get_generated_slots.boolean = True 165 | 166 | list_display = [ 167 | "teacher", 168 | "date", 169 | "start_time", 170 | "end_time", 171 | 'get_generated_slots' 172 | ] 173 | list_filter = ["date", "start_time", "end_time"] 174 | search_fields = ["teacher"] 175 | ordering = ['date', 'start_time'] 176 | 177 | admin.site.register(InterviewerFreeTime, InterviewerFreeTimeAdmin) 178 | 179 | 180 | class InterviewSlotAdmin(admin.ModelAdmin): 181 | 182 | # Generate interview slots using the free time of the teachers(interviewers) 183 | 184 | def generate_slots(self, request): 185 | if request.user.is_superuser: 186 | interview_length = 20 187 | break_between_interviews = 10 188 | 189 | interview_slots_generator = GenerateInterviewSlots( 190 | interview_length, break_between_interviews) 191 | 192 | interview_slots_generator.generate_interview_slots() 193 | generated_slots = interview_slots_generator.get_generated_slots() 194 | 195 | self.message_user( 196 | request, "%s slots were generated" 197 | % generated_slots) 198 | 199 | return HttpResponseRedirect(request.META["HTTP_REFERER"]) 200 | else: 201 | return HttpResponseForbidden() 202 | 203 | # Generate interviews using the free slots and the students without interviews 204 | def generate_interviews(self, request): 205 | if request.user.is_superuser: 206 | interview_generator = GenerateInterviews() 207 | interview_generator.generate_interviews() 208 | 209 | generated_interviews = interview_generator.get_generated_interviews_count() 210 | students_without_interviews = interview_generator.get_students_without_interviews() 211 | 212 | self.message_user( 213 | request, "%s interviews were generated" 214 | % generated_interviews) 215 | self.message_user( 216 | request, "%s students do not have interview date" 217 | % students_without_interviews) 218 | 219 | return HttpResponseRedirect(request.META["HTTP_REFERER"]) 220 | else: 221 | return HttpResponseForbidden() 222 | 223 | # Generate emails for interview date confirmation 224 | def generate_emails(self, request): 225 | if request.user.is_superuser: 226 | # Generate emails for the new courses 227 | # template = 'new_courses' 228 | # email_generator = GenerateNewCoursesEmails(template) 229 | # email_generator.generate_new_courses_emails() 230 | 231 | template = 'interview_confirmation' 232 | 233 | email_generator = GenerateConfirmEmails( 234 | template, confirm_interview_url, choose_interview_url) 235 | 236 | email_generator.generate_confirmation_emails() 237 | 238 | generated_emails = email_generator.get_generated_emails() 239 | students_with_emails = email_generator.get_students_with_generated_emails() 240 | students_without_emails = email_generator.get_students_without_generated_emails() 241 | 242 | self.message_user( 243 | request, "%s confirmational emails were generated" 244 | % generated_emails) 245 | self.message_user( 246 | request, "There are %s students with generated confirmational emails" 247 | % students_with_emails) 248 | self.message_user( 249 | request, "%s students still do NOT have confirmational emails" 250 | % students_without_emails) 251 | return HttpResponseRedirect(request.META["HTTP_REFERER"]) 252 | else: 253 | return HttpResponseForbidden() 254 | 255 | def get_urls(self): 256 | urls = super().get_urls() 257 | my_urls = patterns( 258 | "", 259 | url(r"^generate_slots/$", self.generate_slots, name="generate_slots"), 260 | url(r"^generate_interviews/$", self.generate_interviews, name="generate_interviews"), 261 | url(r"^generate_emails/$", self.generate_emails, name="generate_emails") 262 | ) 263 | return my_urls + urls 264 | 265 | def has_change_permission(self, request, obj=None): 266 | if obj and request.POST and not request.user.is_superuser: 267 | return False 268 | return super().has_change_permission(request, obj) 269 | 270 | def get_queryset(self, request): 271 | queryset = super().get_queryset(request) 272 | if request.user.is_superuser: 273 | return queryset 274 | return queryset.filter( 275 | teacher_time_slot=request.user.interviewerfreetime_set.all()) 276 | 277 | def get_date(self, obj): 278 | return obj.teacher_time_slot.date 279 | get_date.short_description = 'Date' 280 | get_date.admin_order_field = 'teacher_time_slot__date' 281 | 282 | def get_start_time(self, obj): 283 | return obj.start_time 284 | get_start_time.short_description = "Starting" 285 | get_start_time.admin_order_field = "start_time" 286 | 287 | def get_student(self, obj): 288 | if obj.student_id and obj.student.name: 289 | return u"{1}".format(obj.student_id, obj.student.name) 290 | return 291 | get_student.short_description = "Student" 292 | get_student.allow_tags = True 293 | 294 | def get_teacher(self, obj): 295 | return obj.teacher_time_slot.teacher 296 | get_teacher.short_description = "Teacher" 297 | 298 | def get_student_confirmation(self, obj): 299 | if obj.student_id: 300 | return obj.student.has_confirmed_interview 301 | return 302 | get_student_confirmation.short_description = "Confirmed interview" 303 | get_student_confirmation.boolean = True 304 | 305 | def get_student_has_been_interviewed(self, obj): 306 | if obj.student_id: 307 | return obj.student.has_been_interviewed 308 | return 309 | get_student_has_been_interviewed.short_description = "Has been interviewed" 310 | get_student_has_been_interviewed.boolean = True 311 | 312 | def get_teacher_skype(self, obj): 313 | return obj.teacher_time_slot.teacher.skype 314 | get_teacher_skype.short_description = "Teacher Skype" 315 | 316 | def get_student_skype(self, obj): 317 | if obj.student_id: 318 | return obj.student.skype 319 | return 320 | get_student_skype.short_description = "Student Skype" 321 | 322 | def get_student_email(self, obj): 323 | if obj.student_id: 324 | return obj.student.email 325 | return 326 | get_student_email.short_description = "Student Email" 327 | 328 | def get_student_course(self, obj): 329 | if obj.student_id: 330 | return obj.student.applied_course 331 | return 332 | get_student_course.short_description = "Applying for" 333 | 334 | def get_is_student_accepted(self, obj): 335 | if obj.student_id: 336 | return obj.student.is_accepted 337 | return 338 | get_is_student_accepted.short_description = "Is accepted" 339 | get_is_student_accepted.boolean = True 340 | 341 | def get_student_code_skills(self, obj): 342 | if obj.student_id: 343 | return obj.student.code_skills_rating 344 | return 345 | get_student_code_skills.short_description = "Code skills" 346 | 347 | def get_student_code_design_rating(self, obj): 348 | if obj.student_id: 349 | return obj.student.code_design_rating 350 | return 351 | get_student_code_design_rating.short_description = "Design skills" 352 | 353 | def get_student_fit_attitude_rating(self, obj): 354 | if obj.student_id: 355 | return obj.student.fit_attitude_rating 356 | return 357 | get_student_fit_attitude_rating.short_description = "Fit rating" 358 | 359 | list_display = [ 360 | 'get_date', 361 | 'get_start_time', 362 | 'get_student', 363 | 'get_student_skype', 364 | 'get_teacher', 365 | 'get_teacher_skype', 366 | 'get_student_course', 367 | 'get_student_confirmation', 368 | 'get_student_has_been_interviewed', 369 | 'get_student_code_skills', 370 | 'get_student_code_design_rating', 371 | 'get_student_fit_attitude_rating', 372 | 'get_is_student_accepted', 373 | ] 374 | ordering = ['teacher_time_slot__date', 'start_time'] 375 | 376 | admin.site.register(InterviewSlot, InterviewSlotAdmin) 377 | -------------------------------------------------------------------------------- /source/course_interviews/tests/test_manage.py: -------------------------------------------------------------------------------- 1 | from course_interviews.models import Student, Teacher, InterviewerFreeTime, InterviewSlot 2 | from course_interviews.helpers.generate_interview_slots import GenerateInterviewSlots 3 | from course_interviews.helpers.generate_interviews import GenerateInterviews 4 | from course_interviews.helpers.generate_emails import GenerateConfirmEmails 5 | from post_office.models import EmailTemplate 6 | from django.contrib.auth.models import Group, Permission 7 | from django.core.urlresolvers import reverse 8 | from django.test import TestCase, Client 9 | from datetime import date, timedelta 10 | 11 | 12 | class ManagePyGenerateSlotsTests(TestCase): 13 | 14 | def setUp(self): 15 | self.teacher_admin = Teacher.objects.create_superuser( 16 | "admin@admin.com", "123", skype="admin_hackbulgaria") 17 | 18 | self.teacher_user1 = Teacher.objects.create_user( 19 | "user1@user.com", "123", skype="user1_user") 20 | 21 | self.teacher_user2 = Teacher.objects.create_user( 22 | "user2@user.com", "123", skype="user2_user") 23 | 24 | teacher_user_permission_names = [ 25 | 'add_interviewerfreetime', 26 | 'change_interviewerfreetime', 27 | 'delete_interviewerfreetime', 28 | 'change_interviewslot', 29 | 'add_student', 30 | 'change_student', 31 | ] 32 | 33 | teacher_user_permissions = Permission.objects.filter( 34 | codename__in=teacher_user_permission_names 35 | ) 36 | 37 | self.teacher_group = Group.objects.create(name='Editor') 38 | self.teacher_group.permissions = teacher_user_permissions 39 | self.teacher_group.save() 40 | 41 | self.teacher_user1.first_name = "Ivo" 42 | self.teacher_user1.last_name = "Radov" 43 | self.teacher_user1.is_staff = True 44 | self.teacher_user1.groups.add(self.teacher_group) 45 | self.teacher_user1.save() 46 | 47 | self.teacher_user2.first_name = "Rado" 48 | self.teacher_user2.last_name = "Ivov" 49 | self.teacher_user2.is_staff = True 50 | self.teacher_user2.groups.add(self.teacher_group) 51 | self.teacher_user2.save() 52 | 53 | self.tomorrow = date.today() + timedelta(days=1) 54 | 55 | self.teacher_free_time1 = InterviewerFreeTime.objects.create( 56 | teacher=self.teacher_user1, 57 | date=str(self.tomorrow), 58 | start_time="15:00", 59 | end_time="16:00") 60 | 61 | self.teacher_free_time2 = InterviewerFreeTime.objects.create( 62 | teacher=self.teacher_user2, 63 | date=str(self.tomorrow), 64 | start_time="16:00", 65 | end_time="17:00") 66 | 67 | self.interview_slot1 = InterviewSlot.objects.create( 68 | teacher_time_slot=self.teacher_free_time1, 69 | start_time="15:00") 70 | 71 | self.interview_slot2 = InterviewSlot.objects.create( 72 | teacher_time_slot=self.teacher_free_time2, 73 | start_time="16:00") 74 | 75 | def test_generate_slots_for_interviewer_free_time_without_generated_slots(self): 76 | self.test_teacher_user = Teacher.objects.create_user( 77 | "testuser@user.com", "123", skype="testuser_user") 78 | 79 | self.test_teacher_user.first_name = "Test" 80 | self.test_teacher_user.last_name = "Testov" 81 | self.test_teacher_user.is_staff = True 82 | self.test_teacher_user.groups.add(self.teacher_group) 83 | self.test_teacher_user.save() 84 | 85 | InterviewerFreeTime.objects.create( 86 | teacher=self.test_teacher_user, 87 | date=str(self.tomorrow), 88 | start_time="15:00", 89 | end_time="17:00") 90 | 91 | interview_length = 30 92 | break_between_interviews = 10 93 | 94 | interview_slots_generator = GenerateInterviewSlots( 95 | interview_length, break_between_interviews) 96 | 97 | # For a total duration of 2 hours and interview_length + break = 40 minutes 98 | # a total of three interview slots should be generated 99 | interview_slots_generator.generate_interview_slots() 100 | 101 | client = Client() 102 | client.login( 103 | email=self.test_teacher_user.email, 104 | password='123' 105 | ) 106 | 107 | url = reverse('admin:course_interviews_interviewslot_changelist') 108 | response = client.get(url, follow=True) 109 | 110 | result_list = response.context_data['cl'].result_list 111 | 112 | # The new teacher should see his three newly generated slots 113 | self.assertEqual(len(result_list), 3) 114 | 115 | def test_generate_slots_for_interviewer_free_time_with_generated_slots(self): 116 | """ 117 | No interview slots should be generated, if the interviewer 118 | free time already has existing slots generated for that period 119 | """ 120 | client = Client() 121 | client.login( 122 | email=self.teacher_admin.email, 123 | password='123' 124 | ) 125 | url = reverse('admin:course_interviews_interviewslot_changelist') 126 | response = client.get(url, follow=True) 127 | 128 | result_list_before_slot_generation = response.context_data['cl'].result_list 129 | 130 | interview_length = 30 131 | break_between_interviews = 10 132 | interview_slots_generator = GenerateInterviewSlots( 133 | interview_length, break_between_interviews) 134 | interview_slots_generator.generate_interview_slots() 135 | 136 | response = client.get(url, follow=True) 137 | result_list_after_slot_generation = response.context_data['cl'].result_list 138 | 139 | self.assertCountEqual(result_list_before_slot_generation, result_list_after_slot_generation) 140 | 141 | def test_generated_interviews_proper_length(self): 142 | self.test_teacher_user = Teacher.objects.create_user( 143 | "testuser@user.com", "123", skype="testuser_user") 144 | 145 | self.test_teacher_user.first_name = "Test" 146 | self.test_teacher_user.last_name = "Testov" 147 | self.test_teacher_user.is_staff = True 148 | self.test_teacher_user.groups.add(self.teacher_group) 149 | self.test_teacher_user.save() 150 | 151 | interview_length = 20 152 | break_between_interviews = 10 153 | interview_slots_generator = GenerateInterviewSlots( 154 | interview_length, break_between_interviews) 155 | 156 | time_slot1 = InterviewerFreeTime.objects.create( 157 | teacher=self.test_teacher_user, 158 | date=str(self.tomorrow), 159 | start_time="15:00", 160 | end_time="15:15") 161 | 162 | # Not enough time for an interview, no slots should be generated 163 | interview_slots_generator.generate_interview_slots() 164 | slots = InterviewSlot.objects.all().filter(teacher_time_slot=time_slot1).count() 165 | 166 | self.assertEqual(slots, 0) 167 | 168 | time_slot2 = InterviewerFreeTime.objects.create( 169 | teacher=self.test_teacher_user, 170 | date=str(self.tomorrow), 171 | start_time="15:00", 172 | end_time="15:20") 173 | 174 | # Generate one interview slot 175 | interview_slots_generator.generate_interview_slots() 176 | slots = InterviewSlot.objects.all().filter(teacher_time_slot=time_slot2).count() 177 | 178 | self.assertEqual(slots, 1) 179 | 180 | time_slot3 = InterviewerFreeTime.objects.create( 181 | teacher=self.test_teacher_user, 182 | date=str(self.tomorrow), 183 | start_time="15:00", 184 | end_time="17:00") 185 | # Generate 4 more interview slots 186 | interview_slots_generator.generate_interview_slots() 187 | slots = InterviewSlot.objects.all().filter(teacher_time_slot=time_slot3).count() 188 | 189 | self.assertEqual(slots, 4) 190 | 191 | def test_generate_slots_for_buffer_teacher_free_time(self): 192 | InterviewerFreeTime.objects.create( 193 | teacher=self.teacher_user2, 194 | date=str(self.tomorrow), 195 | start_time="16:00", 196 | end_time="17:00", 197 | buffer_time=True) 198 | 199 | interview_length = 20 200 | break_between_interviews = 10 201 | 202 | interview_slots_generator = GenerateInterviewSlots( 203 | interview_length, break_between_interviews) 204 | interview_slots_generator.generate_interview_slots() 205 | 206 | buffer_slot = InterviewSlot.objects.latest('id') 207 | self.assertEqual(buffer_slot.buffer_slot, True) 208 | 209 | def test_generate_slots_for_non_buffer_teacher_free_time(self): 210 | InterviewerFreeTime.objects.create( 211 | teacher=self.teacher_user2, 212 | date=str(self.tomorrow), 213 | start_time="16:00", 214 | end_time="17:00", 215 | buffer_time=False) 216 | 217 | interview_length = 20 218 | break_between_interviews = 10 219 | 220 | interview_slots_generator = GenerateInterviewSlots( 221 | interview_length, break_between_interviews) 222 | interview_slots_generator.generate_interview_slots() 223 | 224 | buffer_slot = InterviewSlot.objects.latest('id') 225 | self.assertEqual(buffer_slot.buffer_slot, False) 226 | 227 | 228 | class ManagePyGenerateInterviewsTests(TestCase): 229 | 230 | def setUp(self): 231 | self.teacher_admin = Teacher.objects.create_superuser( 232 | "admin@admin.com", "123", skype="admin_hackbulgaria") 233 | 234 | self.teacher_user1 = Teacher.objects.create_user( 235 | "user1@user.com", "123", skype="user1_user") 236 | 237 | teacher_user_permission_names = [ 238 | 'add_interviewerfreetime', 239 | 'change_interviewerfreetime', 240 | 'delete_interviewerfreetime', 241 | 'change_interviewslot', 242 | 'add_student', 243 | 'change_student', 244 | ] 245 | 246 | teacher_user_permissions = Permission.objects.filter( 247 | codename__in=teacher_user_permission_names 248 | ) 249 | 250 | self.teacher_group = Group.objects.create(name='Editor') 251 | self.teacher_group.permissions = teacher_user_permissions 252 | self.teacher_group.save() 253 | 254 | self.teacher_user1.first_name = "Ivo" 255 | self.teacher_user1.last_name = "Radov" 256 | self.teacher_user1.is_staff = True 257 | self.teacher_user1.groups.add(self.teacher_group) 258 | self.teacher_user1.save() 259 | 260 | self.student1 = Student.objects.create( 261 | name="Student One", 262 | email="student1@student.com", 263 | skype="student_one_skype") 264 | 265 | self.student2 = Student.objects.create( 266 | name="Student Two", 267 | email="student2@student.com", 268 | skype="student_two_skype") 269 | 270 | self.student3 = Student.objects.create( 271 | name="Student Three", 272 | email="student3@student.com", 273 | skype="student_three_skype") 274 | 275 | def test_generate_interviews_for_students_without_interview_date(self): 276 | # Create valid time slot 277 | tomorrow = date.today() + timedelta(days=1) 278 | InterviewerFreeTime.objects.create( 279 | teacher=self.teacher_user1, 280 | date=str(tomorrow), 281 | start_time="15:00", 282 | end_time="16:00") 283 | 284 | interview_length = 20 285 | break_between_interviews = 10 286 | 287 | interview_slots_generator = GenerateInterviewSlots( 288 | interview_length, break_between_interviews) 289 | interview_slots_generator.generate_interview_slots() 290 | 291 | interview_generator = GenerateInterviews() 292 | interview_generator.generate_interviews() 293 | client = Client() 294 | client.login( 295 | email=self.teacher_admin.email, 296 | password='123' 297 | ) 298 | 299 | # There are only two slots, so only the first two students should have interview dates 300 | self.assertEqual(Student.objects.get(id=self.student1.id).has_interview_date, True) 301 | self.assertEqual(Student.objects.get(id=self.student2.id).has_interview_date, True) 302 | self.assertEqual(Student.objects.get(id=self.student3.id).has_interview_date, False) 303 | 304 | def test_generate_interviews_for_students_with_interview_date(self): 305 | """ 306 | If a student has interview slot, he should not receive another one 307 | He can change his slot only through the choose-interview url 308 | """ 309 | # Create valid time slot 310 | tomorrow = date.today() + timedelta(days=1) 311 | InterviewerFreeTime.objects.create( 312 | teacher=self.teacher_user1, 313 | date=str(tomorrow), 314 | start_time="15:00", 315 | end_time="16:00") 316 | 317 | interview_length = 20 318 | break_between_interviews = 10 319 | 320 | interview_slots_generator = GenerateInterviewSlots( 321 | interview_length, break_between_interviews) 322 | interview_slots_generator.generate_interview_slots() 323 | 324 | self.student1.has_interview_date = True 325 | self.student1.save() 326 | 327 | interview_generator = GenerateInterviews() 328 | interview_generator.generate_interviews() 329 | client = Client() 330 | client.login( 331 | email=self.teacher_admin.email, 332 | password='123' 333 | ) 334 | 335 | # With two slots available and student1 with interview date, 336 | # all three students should have interview dates 337 | self.assertEqual(Student.objects.get(id=self.student1.id).has_interview_date, True) 338 | self.assertEqual(Student.objects.get(id=self.student2.id).has_interview_date, True) 339 | self.assertEqual(Student.objects.get(id=self.student3.id).has_interview_date, True) 340 | 341 | def test_generate_interview_for_slot_date_before_today(self): 342 | """ 343 | Interviews for slots with date < today should not be generated 344 | """ 345 | # Create outdated time slot 346 | yesterday = date.today() - timedelta(days=1) 347 | InterviewerFreeTime.objects.create( 348 | teacher=self.teacher_user1, 349 | date=str(yesterday), 350 | start_time="15:00", 351 | end_time="15:30") 352 | 353 | # Create valid time slot 354 | tomorrow = date.today() + timedelta(days=1) 355 | InterviewerFreeTime.objects.create( 356 | teacher=self.teacher_user1, 357 | date=str(tomorrow), 358 | start_time="15:00", 359 | end_time="15:30") 360 | 361 | interview_length = 20 362 | break_between_interviews = 10 363 | 364 | interview_slots_generator = GenerateInterviewSlots( 365 | interview_length, break_between_interviews) 366 | interview_slots_generator.generate_interview_slots() 367 | 368 | interview_generator = GenerateInterviews() 369 | interview_generator.generate_interviews() 370 | 371 | yesterday_slot = InterviewSlot.objects.all().filter(teacher_time_slot__date=yesterday) 372 | yesterday_slot = yesterday_slot[0] 373 | 374 | tomorrow_slot = InterviewSlot.objects.all().filter(teacher_time_slot__date=tomorrow) 375 | tomorrow_slot = tomorrow_slot[0] 376 | 377 | self.assertEqual(yesterday_slot.student, None) 378 | self.assertEqual(tomorrow_slot.student, self.student1) 379 | 380 | def test_generate_interview_for_slot_date_that_is_today(self): 381 | """ 382 | Interviews for slots with date equal to today should not be generated 383 | """ 384 | # Create time slot for today 385 | today = date.today() 386 | InterviewerFreeTime.objects.create( 387 | teacher=self.teacher_user1, 388 | date=str(today), 389 | start_time="15:00", 390 | end_time="15:30") 391 | 392 | # Create valid time slot 393 | tomorrow = date.today() + timedelta(days=1) 394 | InterviewerFreeTime.objects.create( 395 | teacher=self.teacher_user1, 396 | date=str(tomorrow), 397 | start_time="15:00", 398 | end_time="15:30") 399 | 400 | interview_length = 20 401 | break_between_interviews = 10 402 | 403 | interview_slots_generator = GenerateInterviewSlots( 404 | interview_length, break_between_interviews) 405 | interview_slots_generator.generate_interview_slots() 406 | 407 | interview_generator = GenerateInterviews() 408 | interview_generator.generate_interviews() 409 | 410 | today_slot = InterviewSlot.objects.all().filter(teacher_time_slot__date=today) 411 | today_slot = today_slot[0] 412 | 413 | tomorrow_slot = InterviewSlot.objects.all().filter(teacher_time_slot__date=tomorrow) 414 | tomorrow_slot = tomorrow_slot[0] 415 | 416 | self.assertEqual(today_slot.student, None) 417 | self.assertEqual(tomorrow_slot.student, self.student1) 418 | 419 | 420 | class ManagePyGenerateEmailsTests(TestCase): 421 | 422 | def setUp(self): 423 | self.teacher_admin = Teacher.objects.create_superuser( 424 | "admin@admin.com", "123", skype="admin_hackbulgaria") 425 | 426 | self.teacher_user1 = Teacher.objects.create_user( 427 | "user1@user.com", "123", skype="user1_user") 428 | 429 | self.teacher_user2 = Teacher.objects.create_user( 430 | "user2@user.com", "123", skype="user2_user") 431 | 432 | teacher_user_permission_names = [ 433 | 'add_interviewerfreetime', 434 | 'change_interviewerfreetime', 435 | 'delete_interviewerfreetime', 436 | 'change_interviewslot', 437 | 'add_student', 438 | 'change_student', 439 | ] 440 | 441 | teacher_user_permissions = Permission.objects.filter( 442 | codename__in=teacher_user_permission_names 443 | ) 444 | 445 | self.teacher_group = Group.objects.create(name='Editor') 446 | self.teacher_group.permissions = teacher_user_permissions 447 | self.teacher_group.save() 448 | 449 | self.teacher_user1.first_name = "Ivo" 450 | self.teacher_user1.last_name = "Radov" 451 | self.teacher_user1.is_staff = True 452 | self.teacher_user1.groups.add(self.teacher_group) 453 | self.teacher_user1.save() 454 | 455 | self.teacher_user2.first_name = "Rado" 456 | self.teacher_user2.last_name = "Ivov" 457 | self.teacher_user2.is_staff = True 458 | self.teacher_user2.groups.add(self.teacher_group) 459 | self.teacher_user2.save() 460 | 461 | self.tomorrow = date.today() + timedelta(days=1) 462 | 463 | self.teacher_free_time1 = InterviewerFreeTime.objects.create( 464 | teacher=self.teacher_user1, 465 | date=str(self.tomorrow), 466 | start_time="15:00", 467 | end_time="16:00") 468 | 469 | self.teacher_free_time2 = InterviewerFreeTime.objects.create( 470 | teacher=self.teacher_user2, 471 | date=str(self.tomorrow), 472 | start_time="16:00", 473 | end_time="17:00") 474 | 475 | self.interview_slot1 = InterviewSlot.objects.create( 476 | teacher_time_slot=self.teacher_free_time1, 477 | start_time="15:00") 478 | 479 | self.interview_slot2 = InterviewSlot.objects.create( 480 | teacher_time_slot=self.teacher_free_time2, 481 | start_time="16:00") 482 | 483 | self.student1 = Student.objects.create( 484 | name="Student One", 485 | email="student1@student.com", 486 | skype="student_one_skype") 487 | 488 | self.student2 = Student.objects.create( 489 | name="Student Two", 490 | email="student2@student.com", 491 | skype="student_two_skype") 492 | 493 | self.email_confirmation_template = EmailTemplate.objects.create( 494 | name='test_template', 495 | subject='Morning, Rado', 496 | content='Hi, how are you feeling today?', 497 | html_content='Hi, how are you feeling today?', 498 | ) 499 | 500 | def test_generate_confirmational_email_for_student_with_interview(self): 501 | """ 502 | Students without interview date should not receive email 503 | Students that already received confirmational email 504 | should not receive another one 505 | """ 506 | self.interview_slot1.student = self.student1 507 | self.interview_slot1.save() 508 | self.student1.has_interview_date = True 509 | self.student1.save() 510 | 511 | template = self.email_confirmation_template.name 512 | confirm_interview_url = "confirm-interview-test-url" 513 | choose_interview_url = "choose-interview-test-url" 514 | 515 | confirm_email_generator = GenerateConfirmEmails( 516 | template, confirm_interview_url, choose_interview_url) 517 | 518 | confirm_email_generator.generate_confirmation_emails() 519 | 520 | client = Client() 521 | client.login( 522 | email=self.teacher_admin.email, 523 | password='123' 524 | ) 525 | 526 | url = reverse('admin:post_office_email_changelist') 527 | response = client.get(url, follow=True) 528 | 529 | result_list = response.context_data['cl'].result_list 530 | 531 | # Only email for self.student1 should be in Email change list 532 | self.assertEqual(len(result_list), 1) 533 | self.assertEqual(result_list[0].to[0], self.student1.email) 534 | 535 | def test_generate_confirmational_email_for_student_without_interview(self): 536 | """ 537 | Students without interview date should not receive email 538 | """ 539 | template = self.email_confirmation_template.name 540 | confirm_interview_url = "confirm-interview-test-url" 541 | choose_interview_url = "choose-interview-test-url" 542 | 543 | confirm_email_generator = GenerateConfirmEmails( 544 | template, confirm_interview_url, choose_interview_url) 545 | 546 | confirm_email_generator.generate_confirmation_emails() 547 | 548 | client = Client() 549 | client.login( 550 | email=self.teacher_admin.email, 551 | password='123' 552 | ) 553 | 554 | url = reverse('admin:post_office_email_changelist') 555 | response = client.get(url, follow=True) 556 | 557 | result_list = response.context_data['cl'].result_list 558 | 559 | # All three students dont have interviews 560 | # There should be no emails generated in Email change list 561 | self.assertEqual(len(result_list), 0) 562 | 563 | def test_generate_confirmational_email_for_student_that_already_received_email(self): 564 | """ 565 | Students that already received confirmational email 566 | should not receive another one 567 | """ 568 | self.student2.has_received_email = True 569 | self.student2.save() 570 | 571 | template = self.email_confirmation_template.name 572 | confirm_interview_url = "confirm-interview-test-url" 573 | choose_interview_url = "choose-interview-test-url" 574 | 575 | confirm_email_generator = GenerateConfirmEmails( 576 | template, confirm_interview_url, choose_interview_url) 577 | 578 | confirm_email_generator.generate_confirmation_emails() 579 | 580 | client = Client() 581 | client.login( 582 | email=self.teacher_admin.email, 583 | password='123' 584 | ) 585 | 586 | url = reverse('admin:post_office_email_changelist') 587 | response = client.get(url, follow=True) 588 | 589 | result_list = response.context_data['cl'].result_list 590 | 591 | # Two students dont have interviews and the third already received email 592 | # There should be no emails generated in Email change list 593 | self.assertEqual(len(result_list), 0) 594 | --------------------------------------------------------------------------------