Rapid Router
diff --git a/portal/templates/portal/teach/materials.html b/portal/templates/portal/teach/materials.html
index 898111a1e..70f0b1fc4 100644
--- a/portal/templates/portal/teach/materials.html
+++ b/portal/templates/portal/teach/materials.html
@@ -305,7 +305,7 @@
Key Stage 3
Playing AI:MMO will build on skills learnt in Rapid Router (Key Stages 1 and 2).
The game is a Massively Multiplayer Online (MMO) time travelling adventure, which will take them to the
- next level on their coding journey: Artificial Intelligence and Python.
+ next level on their coding journey: intermediate Python.
Download all KS3 resources
![ZIP ZIP icon]({% static 'portal/img/icon_zip.png' %})
diff --git a/portal/templatetags/app_tags.py b/portal/templatetags/app_tags.py
index 61c41febc..68c85e39c 100644
--- a/portal/templatetags/app_tags.py
+++ b/portal/templatetags/app_tags.py
@@ -39,6 +39,7 @@
from django.template.defaultfilters import stringfilter
from portal.utils import using_two_factor
from portal import beta
+from aimmo.templatetags.players_utils import get_user_playable_games
register = template.Library()
@@ -78,7 +79,7 @@ def is_preview_student(u):
return False
-@register.filter
+@register.filter(name='is_eligible_for_testing')
def is_eligible_for_testing(u):
if is_logged_in_as_teacher(u):
school_set_up = hasattr(u.userprofile.teacher, 'school') and hasattr(u.userprofile.teacher.school, 'eligible_for_testing')
@@ -91,6 +92,11 @@ def has_beta_access(request):
return beta.has_beta_access(request)
+@register.inclusion_tag('portal/partials/aimmo_join_game_dropdown.html', takes_context=True)
+def game_dropdown_list(context, base_url):
+ return get_user_playable_games(context, base_url)
+
+
@register.filter(name='make_into_username')
def make_into_username(u):
username = ''
diff --git a/portal/tests/pageObjects/portal/aimmo_home_page.py b/portal/tests/pageObjects/portal/aimmo_home_page.py
new file mode 100644
index 000000000..129c7896f
--- /dev/null
+++ b/portal/tests/pageObjects/portal/aimmo_home_page.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2018, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from base_page import BasePage
+from selenium.common.exceptions import NoSuchElementException
+
+
+class AimmoHomePage(BasePage):
+ def __init__(self, browser):
+ super(AimmoHomePage, self).__init__(browser)
+
+ assert self.on_correct_page('aimmo_home_page')
+
+ def click_create_new_game_button(self):
+ self.browser.find_element_by_id('create_new_game_button').click()
+
+ def click_create_game_button(self):
+ self.browser.find_element_by_id('create_game_button').click()
+
+ def click_join_game_button(self):
+ self.browser.find_element_by_id('join_game_button').click()
+
+ def click_game_to_join_button(self, game_name):
+ self.browser.find_element_by_link_text(game_name).click()
+
+ def game_exists(self, game_name):
+ try:
+ self.browser.find_element_by_link_text(game_name)
+ return True
+ except NoSuchElementException:
+ return False
+
+ def input_new_game_name(self, new_game_name):
+ self.browser.find_element_by_id('id_name').clear()
+ self.browser.find_element_by_id('id_name').send_keys(new_game_name)
+
+ def get_input_game_name_placeholder(self):
+ return self.browser.find_element_by_id('id_name').get_attribute("placeholder")
diff --git a/portal/tests/pageObjects/portal/base_page.py b/portal/tests/pageObjects/portal/base_page.py
index 8e0b4ce60..4dadf1ac3 100644
--- a/portal/tests/pageObjects/portal/base_page.py
+++ b/portal/tests/pageObjects/portal/base_page.py
@@ -34,7 +34,6 @@
# copyright notice and these terms. You must not misrepresent the origins of this
# program; modified versions of the program must be marked as such and not
# identified as the original program.
-import time
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
@@ -103,6 +102,10 @@ def go_to_resources_page(self):
self.browser.find_element_by_id('resources_button').click()
return resources_page.ResourcesPage(self.browser)
+ def go_to_aimmo_home_page(self):
+ self.browser.find_element_by_id('aimmo_home_button').click()
+ return aimmo_home_page.AimmoHomePage(self.browser)
+
def is_on_admin_login_page(self):
return self.on_correct_page('admin_login')
@@ -131,3 +134,4 @@ def was_form_invalid(self, formID, error):
import resources_page
+import aimmo_home_page
diff --git a/portal/tests/test_preview_users.py b/portal/tests/test_preview_users.py
index 0fcced57e..f2771561a 100644
--- a/portal/tests/test_preview_users.py
+++ b/portal/tests/test_preview_users.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Code for Life
#
-# Copyright (C) 2017, Ocado Limited
+# Copyright (C) 2018, Ocado Limited
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -37,13 +37,18 @@
from django.core.urlresolvers import reverse
from django.test import TestCase, Client
-from utils.teacher import signup_teacher_directly
+from utils.classes import create_class_directly
+from utils.student import create_school_student_directly
+from utils.teacher import signup_teacher_directly, signup_teacher_directly_as_preview_user
from utils.organisation import create_organisation_directly
from portal.models import Teacher
from portal.templatetags.app_tags import is_preview_user, is_eligible_for_testing
+from base_test import BaseTest
+from pageObjects.portal.home_page import HomePage
-class TestPreviewUsers(TestCase):
+
+class UnitTestPreviewUsers(TestCase):
def test_teacher_can_become_tester(self):
email, password = signup_teacher_directly()
@@ -100,3 +105,112 @@ def test_not_eligible_for_testing(self):
_, _ = create_organisation_directly(email, False)
teacher = Teacher.objects.get(new_user__email=email)
self.assertEqual(False, is_eligible_for_testing(teacher.new_user))
+
+ def test_preview_user_can_view_aimmo_home_page(self):
+ email, password = signup_teacher_directly()
+ _, _ = create_organisation_directly(email, True)
+ make_preview_url = reverse('make_preview_tester')
+ c = Client()
+ c.login(username=email, password=password)
+ c.get(make_preview_url)
+ teacher = Teacher.objects.get(new_user__email=email)
+ self.assertEqual(True, is_preview_user(teacher.new_user))
+
+ aimmo_home_page_url = reverse('aimmo')
+ response = c.get(aimmo_home_page_url)
+ self.assertEqual(200, response.status_code)
+
+ def test_not_preview_user_cannot_view_aimmo_home_page(self):
+ email, password = signup_teacher_directly()
+ _, _ = create_organisation_directly(email, False)
+ c = Client()
+ c.login(username=email, password=password)
+ teacher = Teacher.objects.get(new_user__email=email)
+ self.assertEqual(False, is_preview_user(teacher.new_user))
+
+ aimmo_home_page_url = reverse('aimmo')
+ response = c.get(aimmo_home_page_url)
+ self.assertEqual(401, response.status_code)
+
+
+class SeleniumTestPreviewUsers(BaseTest):
+ def test_preview_user_can_create_game(self):
+ email, password = signup_teacher_directly_as_preview_user()
+ create_organisation_directly(email, True)
+ klass, name, access_code = create_class_directly(email)
+ create_school_student_directly(access_code)
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_login_page().login(email, password)
+ page = page.go_to_aimmo_home_page()
+
+ page.click_create_new_game_button()
+ page.input_new_game_name("Test Game")
+ page.click_create_game_button()
+
+ self.assertIn("/aimmo/play/1/", self.selenium.driver.current_url)
+
+ def test_preview_user_cannot_create_empty_game(self):
+ email, password = signup_teacher_directly_as_preview_user()
+ create_organisation_directly(email, True)
+ klass, name, access_code = create_class_directly(email)
+ create_school_student_directly(access_code)
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_login_page().login(email, password)
+ page = page.go_to_aimmo_home_page()
+
+ page.click_create_new_game_button()
+ page.input_new_game_name("")
+ page.click_create_game_button()
+
+ self.assertEqual(page.get_input_game_name_placeholder(), "Give your new game a name...")
+
+ def test_preview_user_cannot_create_duplicate_game(self):
+ email, password = signup_teacher_directly_as_preview_user()
+ create_organisation_directly(email, True)
+ klass, name, access_code = create_class_directly(email)
+ create_school_student_directly(access_code)
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_login_page().login(email, password)
+ page = page.go_to_aimmo_home_page()
+
+ page.click_create_new_game_button()
+ page.input_new_game_name("Test Game")
+ page.click_create_game_button()
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_aimmo_home_page()
+
+ page.click_create_new_game_button()
+ page.input_new_game_name("Test Game")
+ page.click_create_game_button()
+
+ self.assertEqual(page.get_input_game_name_placeholder(), "Sorry, a game with this name already exists...")
+
+ def test_preview_user_can_join_game(self):
+ email, password = signup_teacher_directly_as_preview_user()
+ create_organisation_directly(email, True)
+ klass, name, access_code = create_class_directly(email)
+ create_school_student_directly(access_code)
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_login_page().login(email, password)
+ page = page.go_to_aimmo_home_page()
+
+ new_game_name = "Join me"
+ page.click_create_new_game_button()
+ page.input_new_game_name(new_game_name)
+ page.click_create_game_button()
+
+ self.selenium.get(self.live_server_url)
+ page = HomePage(self.selenium).go_to_aimmo_home_page()
+
+ page.click_join_game_button()
+
+ assert page.game_exists(new_game_name)
+
+ page.click_game_to_join_button(new_game_name)
+
+ self.assertIn("/aimmo/play/2/", self.selenium.driver.current_url)
diff --git a/portal/tests/utils/teacher.py b/portal/tests/utils/teacher.py
index b1b4f01c2..ea3713d34 100644
--- a/portal/tests/utils/teacher.py
+++ b/portal/tests/utils/teacher.py
@@ -61,6 +61,15 @@ def signup_teacher_directly(**kwargs):
return email_address, password
+def signup_teacher_directly_as_preview_user(**kwargs):
+ title, first_name, last_name, email_address, password = generate_details(**kwargs)
+ teacher = Teacher.objects.factory(title, first_name, last_name, email_address, password)
+ generate_token(teacher.new_user, preverified=True)
+ teacher.user.set_to_preview_user()
+ teacher.user.save()
+ return email_address, password
+
+
def signup_duplicate_teacher_fail(page, duplicate_email):
page = page.go_to_signup_page()
diff --git a/portal/urls.py b/portal/urls.py
index e3ecf8c6d..37bd9287c 100644
--- a/portal/urls.py
+++ b/portal/urls.py
@@ -46,6 +46,7 @@
from portal.views.admin import aggregated_data, schools_map, admin_login
from portal.views.teacher.solutions_level_selector import levels
from portal.permissions import teacher_verified
+from portal.views.aimmo.home import aimmo_home
from portal.views.email import send_new_users_report
@@ -64,6 +65,9 @@
teacher_accept_student_request
from portal.views.registration import teacher_password_reset, password_reset_done, student_password_reset, \
password_reset_check_and_confirm, custom_2FA_login
+
+from aimmo.urls import HOMEPAGE_REGEX
+
js_info_dict = {
'packages': ('conf.locale',),
}
@@ -83,7 +87,13 @@
urlpatterns = [
- url(r'^aimmo/', include('aimmo.urls')),
+ # The first AIMMO URL renders the new AIMMO home page. It uses the same regex so as to overwrite the default
+ # home page in the AIMMO project.
+ # The second AIMMO URL imports all the URLs from the AIMMO project.
+ url(HOMEPAGE_REGEX, aimmo_home, name='aimmo'),
+ url(HOMEPAGE_REGEX, include('aimmo.urls')),
+
+
url(r'^favicon\.ico$', RedirectView.as_view(url='/static/portal/img/favicon.ico', permanent=True)),
url(r'^administration/login/$', admin_login, name='administration_login'),
diff --git a/portal/views/aimmo/__init__.py b/portal/views/aimmo/__init__.py
new file mode 100644
index 000000000..ff0d4897f
--- /dev/null
+++ b/portal/views/aimmo/__init__.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2018, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
diff --git a/portal/views/aimmo/home.py b/portal/views/aimmo/home.py
new file mode 100644
index 000000000..9533f4b57
--- /dev/null
+++ b/portal/views/aimmo/home.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2018, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from django.shortcuts import render, redirect
+from django.core.urlresolvers import reverse_lazy
+from django.contrib.auth.decorators import login_required
+
+from portal.permissions import preview_user
+from portal.views.teacher.teach import get_session_pdfs, get_resource_sheets_pdfs
+
+from aimmo.app_settings import get_users_for_new_game
+from aimmo.forms import AddGameForm
+
+
+def save_form(request, create_game_form):
+ game = create_game_form.save(commit=False)
+ game.generator = 'Main'
+ game.owner = request.user
+ game.main_user = request.user
+ game.save()
+ users = get_users_for_new_game(request)
+
+ if users is not None:
+ game.can_play.add(*users)
+ return redirect('aimmo/play', id=game.id)
+
+
+@login_required(login_url=reverse_lazy('login_view'))
+@preview_user
+def aimmo_home(request):
+ ks3_sessions = []
+ ks3_sheets = []
+
+ get_session_pdfs("ks3_session_", ks3_sessions)
+ get_resource_sheets_pdfs(ks3_sessions, "KS3_S", ks3_sheets)
+
+ playable_games = request.user.playable_games.all()
+
+ if request.method == 'POST':
+ create_game_form = AddGameForm(playable_games, data=request.POST)
+ if create_game_form.is_valid():
+ return save_form(request, create_game_form)
+
+ else:
+ create_game_form = AddGameForm(playable_games)
+
+ return render(request, 'portal/aimmo_home.html',
+ {'create_game_form': create_game_form,
+ 'ks3_sessions': ks3_sessions,
+ 'ks3_sheets': ks3_sheets})
diff --git a/portal/views/home.py b/portal/views/home.py
index b5f673d14..3c1c9cbdf 100644
--- a/portal/views/home.py
+++ b/portal/views/home.py
@@ -460,6 +460,6 @@ def make_preview_tester(request):
user.set_to_preview_user()
user.save()
- return HttpResponseRedirect(reverse_lazy('play_aimmo'))
+ return HttpResponseRedirect(reverse_lazy('aimmo'))
return HttpResponse(status=401)
return HttpResponse(status=405)
diff --git a/portal/views/teacher/pdfs.py b/portal/views/teacher/pdfs.py
index b91adc41f..15ce98583 100644
--- a/portal/views/teacher/pdfs.py
+++ b/portal/views/teacher/pdfs.py
@@ -907,7 +907,7 @@
"page_origin": "#uks2-assessments"
},
- # ...................KS3 sessions...........#
+ # ...KS3 sessions...#
"ks3_session_1": {
"title": "MMO",
"description": "AI:MMO: the MMO Basics",
@@ -916,7 +916,7 @@
"page_origin": "#ks3-sessions"
},
- # ..............KS3 Resource Sheets...........#
+ # ...KS3 Resource Sheets...#
# ...Session 1...#
"KS3_S1_1": {
"title": "Code cheat sheet",
@@ -926,7 +926,7 @@
"page_origin": "#ks3-resource-sheets"
},
"KS3_S1_2": {
- "title": "Indentation - Why it's important",
+ "title": "Indentation — Why it matters",
"description": "To be used to understand how to use Python to code the AI:MMO game.",
"links": [],
"url": "KS3/resource_sheets/KS3-PythonIndentation.pdf",
@@ -952,4 +952,27 @@
"page_origin": "#ks3-assessments"
},
+ # ...KS3 Challenges...#
+ "KS3_challenges_guide": {
+ "title": "Teacher lesson guide for challenges",
+ "description": "To be used to code the AI:MMO game.",
+ "links": [],
+ "url": "KS3/challenges/KS3-challenges-guide.pdf",
+ "page_origin": "#ks3-challenges"
+ },
+ "KS3_challenge1": {
+ "title": "AI:MMO Challenge Sheet 1",
+ "description": "To be used to understand how to use Python to code the AI:MMO game.",
+ "links": [],
+ "url": "KS3/challenges/KS3-challenge1.pdf",
+ "page_origin": "#ks3-challenges"
+ },
+ "KS3_challenge2": {
+ "title": "AI:MMO Challenge Sheet 2",
+ "description": "To be used to understand the AI:MMO game.",
+ "links": [],
+ "url": "KS3/challenges/KS3-challenge2.pdf",
+ "page_origin": "#ks3-challenges"
+ },
+
}