aboutsummaryrefslogtreecommitdiffstats
path: root/ctrack
diff options
context:
space:
mode:
authorMatthew Lemon <lemon@matthewlemon.com>2020-05-28 16:12:13 +0100
committerMatthew Lemon <lemon@matthewlemon.com>2020-05-28 16:12:13 +0100
commitacf875c2bca1727306cb70aaa37e6f4595209786 (patch)
tree67390297bcd02b2481f5673629227dfea0884215 /ctrack
parent4aaa68109434e04d1103710d675efd075b2f744a (diff)
parent520a9deb13f93c689443c0339b1c96a67f0c15a1 (diff)
Merge branch 'stakeholder-profile'
Diffstat (limited to 'ctrack')
-rw-r--r--ctrack/conftest.py25
-rw-r--r--ctrack/core/tests/test_functional.py3
-rw-r--r--ctrack/core/utils.py225
-rw-r--r--ctrack/core/views.py17
-rw-r--r--ctrack/organisations/admin.py18
-rw-r--r--ctrack/organisations/migrations/0005_auto_20200525_1502.py29
-rw-r--r--ctrack/organisations/models.py25
-rw-r--r--ctrack/organisations/templates/organisations/organisation_detail.html2
-rw-r--r--ctrack/organisations/tests/factories.py4
-rw-r--r--ctrack/organisations/views.py34
-rw-r--r--ctrack/templates/403.html2
-rw-r--r--ctrack/templates/account/login.html2
-rw-r--r--ctrack/templates/base.html2
-rw-r--r--ctrack/templates/base_stakeholder.html115
-rw-r--r--ctrack/templates/pages/home.html4
-rw-r--r--ctrack/templates/pages/stakeholder_home.html162
-rw-r--r--ctrack/users/admin.py6
-rw-r--r--ctrack/users/migrations/0005_delete_userprofile.py16
-rw-r--r--ctrack/users/migrations/0006_user_stakeholder.py20
-rw-r--r--ctrack/users/models.py22
-rw-r--r--ctrack/users/stakeholder.py4
-rw-r--r--ctrack/users/tests/test_functional.py124
-rw-r--r--ctrack/users/tests/test_models.py10
-rw-r--r--ctrack/users/tests/test_views.py142
-rw-r--r--ctrack/users/views.py12
25 files changed, 861 insertions, 164 deletions
diff --git a/ctrack/conftest.py b/ctrack/conftest.py
index 60a177f..37a4a6c 100644
--- a/ctrack/conftest.py
+++ b/ctrack/conftest.py
@@ -1,11 +1,16 @@
+import os
+
import pytest
from django.test import RequestFactory
+from selenium import webdriver
+from selenium.webdriver.firefox.options import Options
from ctrack.organisations.models import (
Address,
AddressType,
Mode,
Organisation,
+ Stakeholder,
Submode,
)
from ctrack.organisations.tests.factories import (
@@ -35,8 +40,9 @@ def person(user):
submode = Submode.objects.create(descriptor="Light Rail", mode=mode)
org = OrganisationFactory.create(submode=submode)
person = PersonFactory.create(
+ first_name="Toss",
+ last_name="McBride",
role=role,
- updated_by=user,
predecessor=None,
organisation__submode=submode,
organisation=org,
@@ -56,5 +62,22 @@ def addr() -> Address:
@pytest.fixture
+def stakeholder(person):
+ s = Stakeholder.objects.create(person=person)
+ return s
+
+
+@pytest.fixture
def request_factory() -> RequestFactory:
return RequestFactory()
+
+
+@pytest.fixture(scope="module")
+def browser(request):
+ "Provide selenium webdriver instance."
+ os.environ["PATH"] += os.pathsep + os.getcwd()
+ options = Options()
+ options.headless = True
+ browser_ = webdriver.Firefox(firefox_options=options)
+ yield browser_
+ browser_.quit()
diff --git a/ctrack/core/tests/test_functional.py b/ctrack/core/tests/test_functional.py
new file mode 100644
index 0000000..6661072
--- /dev/null
+++ b/ctrack/core/tests/test_functional.py
@@ -0,0 +1,3 @@
+def test_can_get_homepage(browser):
+ browser.get("http://localhost:8000")
+ assert "ctrack" in browser.title
diff --git a/ctrack/core/utils.py b/ctrack/core/utils.py
index 00f8cbd..bebf176 100644
--- a/ctrack/core/utils.py
+++ b/ctrack/core/utils.py
@@ -1,25 +1,36 @@
import random
-from random import randint, choice
+from random import choice, randint
from faker import Faker
-from ctrack.assessments.models import CAFAssessment, CAFObjective, CAFPrinciple, CAFContributingOutcome, \
- CAFAssessmentOutcomeScore, AchievementLevel, IGP
+from ctrack.assessments.models import (
+ IGP,
+ AchievementLevel,
+ CAFAssessment,
+ CAFAssessmentOutcomeScore,
+ CAFContributingOutcome,
+ CAFObjective,
+ CAFPrinciple,
+)
from ctrack.caf.models import CAF
from ctrack.caf.tests.factories import (
+ ApplicableSystemFactory,
+ CAFFactory,
+ FileStoreFactory,
GradingFactory,
- FileStoreFactory, CAFFactory, ApplicableSystemFactory,
)
-from ctrack.organisations.models import AddressType, Person
-from ctrack.organisations.models import Mode
-from ctrack.organisations.models import Submode
-from ctrack.organisations.tests.factories import AddressFactory
-from ctrack.organisations.tests.factories import OrganisationFactory
-from ctrack.organisations.tests.factories import PersonFactory
-from ctrack.organisations.tests.factories import RoleFactory
-from ctrack.organisations.tests.factories import UserFactory
-from ctrack.register.tests.factories import EngagementEventFactory
-from ctrack.register.tests.factories import EngagementTypeFactory
+from ctrack.organisations.models import AddressType, Mode, Person, Submode
+from ctrack.organisations.tests.factories import (
+ AddressFactory,
+ OrganisationFactory,
+ PersonFactory,
+ RoleFactory,
+)
+from ctrack.register.tests.factories import (
+ EngagementEventFactory,
+ EngagementTypeFactory,
+)
+from ctrack.users.tests.factories import UserFactory
fnames = [
"Clock Pylon Systems",
@@ -72,10 +83,7 @@ def populate_db(**kwargs):
submodes = [sb1, sb2, sb3, sb4, sb5, sb6, sb7]
- # we need a User object to completed the updated_by fields in Organisation and Person
- user = (
- UserFactory.create()
- ) # we need to have at least one user for the updated_by field
+ user = UserFactory.create()
# Create 40 Organisation objects
if _org_number:
@@ -100,7 +108,6 @@ def populate_db(**kwargs):
for org in orgs:
PersonFactory.create(
role=choice(roles),
- updated_by=user,
predecessor=None,
organisation__submode=choice(submodes),
organisation=org,
@@ -113,21 +120,18 @@ def populate_db(**kwargs):
# noinspection PyUnboundLocalVariable
p1 = PersonFactory.create(
role=choice(roles),
- updated_by=user,
predecessor=None,
organisation__submode=choice(submodes),
organisation=org,
)
p2 = PersonFactory.create(
role=choice(roles),
- updated_by=user,
predecessor=None,
organisation__submode=choice(submodes),
organisation=org,
)
p3 = PersonFactory.create(
role=choice(roles),
- updated_by=user,
predecessor=None,
organisation__submode=choice(submodes),
organisation=org,
@@ -144,7 +148,6 @@ def populate_db(**kwargs):
inspectors = [
PersonFactory.create(
role=inspector_role,
- updated_by=user,
job_title="Compliance Inspector",
predecessor=None,
organisation__submode=None,
@@ -188,14 +191,26 @@ def populate_db(**kwargs):
)
# We want to simulate 4 CAF Objectives
- c_obj_a = CAFObjective.objects.create(name="Objective A: Managing security risk",
- description="An important objective to fix the world.", order_id=1)
- c_obj_b = CAFObjective.objects.create(name="Objective B: Protecting Against Cyber Attack",
- description="An important objective to fix the world.", order_id=2)
- c_obj_c = CAFObjective.objects.create(name="Objective C: Detecting Cyber Security Events",
- description="An important objective to fix the world.", order_id=3)
- c_obj_d = CAFObjective.objects.create(name="Objective D: Minimising the Impact of Cyber Security Incidents",
- description="An important objective to fix the world.", order_id=4)
+ c_obj_a = CAFObjective.objects.create(
+ name="Objective A: Managing security risk",
+ description="An important objective to fix the world.",
+ order_id=1,
+ )
+ c_obj_b = CAFObjective.objects.create(
+ name="Objective B: Protecting Against Cyber Attack",
+ description="An important objective to fix the world.",
+ order_id=2,
+ )
+ c_obj_c = CAFObjective.objects.create(
+ name="Objective C: Detecting Cyber Security Events",
+ description="An important objective to fix the world.",
+ order_id=3,
+ )
+ c_obj_d = CAFObjective.objects.create(
+ name="Objective D: Minimising the Impact of Cyber Security Incidents",
+ description="An important objective to fix the world.",
+ order_id=4,
+ )
# For each Objective, let's create four Principles
p_a1 = CAFPrinciple.objects.create(
@@ -203,28 +218,28 @@ def populate_db(**kwargs):
designation="A1",
title="Governance",
description="When you don't have Governance, you have nothing.",
- order_id=1
+ order_id=1,
)
p_a2 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_a.id,
designation="A2",
title="Risk Management",
description="Don't take a risk, and don't get nowhere.",
- order_id=2
+ order_id=2,
)
p_a3 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_a.id,
designation="A3",
title="Asset Management",
description="Without assets, you have no raw materials to work with.",
- order_id=3
+ order_id=3,
)
p_a4 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_a.id,
designation="A4",
title="Supply Chain",
description="You need to get your stuff from somewhere.",
- order_id=4
+ order_id=4,
)
p_b1 = CAFPrinciple.objects.create(
@@ -232,28 +247,28 @@ def populate_db(**kwargs):
designation="B1",
title="Service Protection & Policies",
description="Put in place the right protections for a future of security.",
- order_id=1
+ order_id=1,
)
p_b2 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_b.id,
designation="B2",
title="Identity and Access Control",
description="Stop the wrong people getting at your critical assets, okay.",
- order_id=2
+ order_id=2,
)
p_b3 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_b.id,
designation="B3",
title="Data Security",
description="Data is the new oil...",
- order_id=3
+ order_id=3,
)
p_b4 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_b.id,
designation="B4",
title="System Security",
description="If you have complicated systems, they need some sort of security.",
- order_id=4
+ order_id=4,
)
p_b5 = CAFPrinciple.objects.create(
@@ -261,7 +276,7 @@ def populate_db(**kwargs):
designation="B5",
title="Resilience Networks and Systems",
description="When all else fails, there is always food to be cooked.",
- order_id=5
+ order_id=5,
)
p_b6 = CAFPrinciple.objects.create(
@@ -269,7 +284,7 @@ def populate_db(**kwargs):
designation="B6",
title="Staff Awareness and Training",
description="You must ensure your people are trained and equipped for making a difference.",
- order_id=6
+ order_id=6,
)
# Only two of these
@@ -278,14 +293,14 @@ def populate_db(**kwargs):
designation="C1",
title="Security Monitoring",
description="Monitoring the bits and pieces is the most important aspect of your life.",
- order_id=1
+ order_id=1,
)
p_c2 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_c.id,
designation="C2",
title="Proactive Security and Event Discovery",
description="If we're not proactive, we will get found out eventually.",
- order_id=2
+ order_id=2,
)
# Only two of these too
@@ -294,14 +309,14 @@ def populate_db(**kwargs):
designation="D1",
title="Response and Recovery Planning",
description="Responding to the security problems since 1999...",
- order_id=1
+ order_id=1,
)
p_d2 = CAFPrinciple.objects.create(
caf_objective_id=c_obj_d.id,
designation="D2",
title="Improvements",
description="Improving all the things.",
- order_id=2
+ order_id=2,
)
# Based on these principles, it's time to gen some CAFContributingOutcomes
@@ -311,294 +326,290 @@ def populate_db(**kwargs):
name="Board Direction",
description="You have forced your Board to listen to your whinging about cyber.",
principle_id=p_a1.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="A1.b",
name="Roles and Responsibilities",
description="Your elders and betters are impressed and they continue to make money after your project "
- "implementation.",
+ "implementation.",
principle_id=p_a1.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="A1.c",
name="Decision-making",
description="If you are forced to participate in the Crystal Maze, you'll choose the coorect path across "
- "the Gordian runway.",
+ "the Gordian runway.",
principle_id=p_a1.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="A2.a",
name="Risk Management Process",
description="You take mighty risks, but they are mitigated by more sensible people around you - good.",
principle_id=p_a2.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="A2.b",
name="Assurance",
description="We all make mistakes, but in doing this well you at least have told people what you're doing.",
principle_id=p_a2.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="A3.a",
name="Asset Management",
description="Taking care of these aspects of corporate life is commensurate with the money-making way.",
principle_id=p_a3.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="A4.a",
name="Supply Chain",
description="Task your customers to take on all the risk, the debt, the hassle - you're good to go.",
principle_id=p_a4.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B1.a",
name="Policy and Process Development",
description="You are getting your process and policy development spot on.",
principle_id=p_b1.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B1.b",
name="Policy and Process Information",
description="Differs from the above in a few ways that will be discussed at a later date.",
principle_id=p_b1.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="B2.a",
name="ID Verification, Authentication and Authorisation",
description="It is very important for people to be able to confirm they they truly are. Underneath.",
principle_id=p_b2.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B2.b",
name="Device Management",
description="Your devices, and their safe and sustainable use, is crucuial to the longevity of your "
- "company.",
+ "company.",
principle_id=p_b2.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="B2.c",
name="Privileged User Mangement",
description="You ensure that even the most privileged members of your senior management are under the "
- "impression that they exude inequality, in all instances.",
+ "impression that they exude inequality, in all instances.",
principle_id=p_b2.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="B3.a",
name="Understanding Data",
description="You, more than anyone else in the organisation, know what your data means to you.",
principle_id=p_b3.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B3.b",
name="Data in Transit",
description="You are protecting your data as it moves along the Information Superhighway.",
principle_id=p_b3.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="B3.c",
name="Stored Data",
description="You have stored your data in accordance with local environment laws.",
principle_id=p_b3.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="B3.d",
name="Mobile Data",
description="Mobile data is when data moves because it is stored in a moving thing.",
principle_id=p_b3.id,
- order_id=4
+ order_id=4,
),
CAFContributingOutcome.objects.create(
designation="B3.e",
name="Media/Equipment Sanitisation",
description="You routinely wash and clean the legs and bottom brackets of your server racks.",
principle_id=p_b3.id,
- order_id=5
+ order_id=5,
),
CAFContributingOutcome.objects.create(
designation="B4.a",
name="Secure by Design",
description="You have designed your systems to be secure and you're sure no one is going to hack "
- "into them.",
+ "into them.",
principle_id=p_b4.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B4.b",
name="Secure Configuration",
description="When you are able to configure your systems and software well, you can say you have Secure "
- "Configuration. Only then, mind.",
+ "Configuration. Only then, mind.",
principle_id=p_b4.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="B4.c",
name="Secure Management",
description="Somehow this one is different from all the others but I'm not sure how.",
principle_id=p_b4.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="B4.d",
name="Vulnerability Management",
description="Doing this well means that you are at the top of your vulnerability scale.",
principle_id=p_b4.id,
- order_id=4
+ order_id=4,
),
CAFContributingOutcome.objects.create(
designation="B5.a",
name="Resilience Preparation",
description="Totally ready for the coming of the cyber apocalyse. You practice this stuff regular.",
principle_id=p_b5.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B5.b",
name="Design for Resilience",
description="This stuff is built into your very working model.",
principle_id=p_b5.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="B5.c",
name="Backups",
description="There is nowhere for you to go as a professional if you don't make backups of your data.",
principle_id=p_b5.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="B6.a",
name="Cyber Security Culture",
description="You're making them understand that this isn't going to go away in a hurry.",
principle_id=p_b6.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="B6.b",
name="Cyber Security Training",
description="By the way, when youre staff are able to write C code, your company understands buffer "
- "overflows.",
+ "overflows.",
principle_id=p_b6.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="C1.a",
name="Monitoring Coverage",
description="At all times, you are vigilent to the threats out there, and ready to tackle them.",
principle_id=p_c1.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="C1.b",
name="Securing Logs",
description="You might think the are a waste of time, but the Board thinks logging is important.",
principle_id=p_c1.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="C1.c",
name="Generating Alerts",
description="Boo! There, you coped with it because you're good at this.",
principle_id=p_c1.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="C1.d",
name="Identifying Security Incidents",
description="You are wary of all the possible things that could go wrong and you have a plan to deal. Well "
- "done.",
+ "done.",
principle_id=p_c1.id,
- order_id=4
+ order_id=4,
),
CAFContributingOutcome.objects.create(
designation="C1.e",
name="Monitoring Tools and Skills",
description="All these things matter in today's switched on cyber-aware environment.",
principle_id=p_c1.id,
- order_id=5
+ order_id=5,
),
CAFContributingOutcome.objects.create(
designation="C2.a",
name="System Abnormalities for Attack Detection",
description="Make sure you know how to look for things that mighty wrong on your network.",
principle_id=p_c2.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="C2.b",
name="Proactive Attack Discovery",
description="When you go out looking for the bad stuff, you usefully find it - "
- "and you know this in spades.",
+ "and you know this in spades.",
principle_id=p_c2.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="D1.a",
name="Response Plan",
description="Yeah, we know it's boring but you've got to have one.",
principle_id=p_d1.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="D1.b",
name="Response and Recovery Capability",
description="If you can't get back on your feet after you've been beat, where are you, really?",
principle_id=p_d1.id,
- order_id=2
+ order_id=2,
),
CAFContributingOutcome.objects.create(
designation="D1.c",
name="Testing and Exercising",
description="One of the most important things you should not be forgetting is this.",
principle_id=p_d1.id,
- order_id=3
+ order_id=3,
),
CAFContributingOutcome.objects.create(
designation="D2.a",
name="Incident Root Cause and Analysis",
description="I guess there are always lessons learned, no matter how we good we are.",
principle_id=p_d2.id,
- order_id=1
+ order_id=1,
),
CAFContributingOutcome.objects.create(
designation="D2.b",
name="Using Incidents to Drive Improvements",
description="This is the kind of thing that bores us to tears but it simply has to be done.",
principle_id=p_d2.id,
- order_id=2
- )
+ order_id=2,
+ ),
]
achievement_levels = [
AchievementLevel.objects.create(
- descriptor="Not Achieved",
- colour_description="Red",
- colour_hex="#000001"
+ descriptor="Not Achieved", colour_description="Red", colour_hex="#000001"
),
AchievementLevel.objects.create(
descriptor="Partially Achieved",
colour_description="Amber",
- colour_hex="#000002"
+ colour_hex="#000002",
),
AchievementLevel.objects.create(
- descriptor="Achieved",
- colour_description="Green",
- colour_hex="#000003"
- )
+ descriptor="Achieved", colour_description="Green", colour_hex="#000003"
+ ),
]
for al in achievement_levels:
@@ -610,7 +621,7 @@ def populate_db(**kwargs):
IGP.objects.create(
achievement_level=al,
contributing_outcome=co,
- descriptive_text=fake_txt
+ descriptive_text=fake_txt,
)
else:
for co in cos:
@@ -620,7 +631,7 @@ def populate_db(**kwargs):
IGP.objects.create(
achievement_level=al,
contributing_outcome=co,
- descriptive_text=fake_txt
+ descriptive_text=fake_txt,
)
# We want to create a CAF with a bunch of scoring now...
@@ -637,6 +648,10 @@ def populate_db(**kwargs):
CAFAssessmentOutcomeScore.objects.create(
caf_assessment_id=caf_assessment.id,
caf_contributing_outcome_id=c.id,
- assessment_score=random.choice(["Achieved", "Partially Achieved", "Not Achieved"]),
- baseline_assessment_score=random.choice(["Achieved", "Partially Achieved", "Not Achieved"])
+ assessment_score=random.choice(
+ ["Achieved", "Partially Achieved", "Not Achieved"]
+ ),
+ baseline_assessment_score=random.choice(
+ ["Achieved", "Partially Achieved", "Not Achieved"]
+ ),
)
diff --git a/ctrack/core/views.py b/ctrack/core/views.py
index bee2d72..b80d212 100644
--- a/ctrack/core/views.py
+++ b/ctrack/core/views.py
@@ -1,5 +1,20 @@
+from django.contrib.auth.decorators import login_required
from django.shortcuts import render
+from ctrack.organisations.models import Organisation
+
+@login_required
def home_page(request):
- return render(request, "pages/home.html")
+ if request.user.is_stakeholder:
+ org = Organisation.objects.get(
+ name=request.user.stakeholder.person.get_organisation_name()
+ )
+ systems = org.applicablesystem_set.all()
+ return render(
+ request,
+ "pages/stakeholder_home.html",
+ context={"org": org, "systems": systems},
+ )
+ else:
+ return render(request, "pages/home.html")
diff --git a/ctrack/organisations/admin.py b/ctrack/organisations/admin.py
index dbd7239..f85ddad 100644
--- a/ctrack/organisations/admin.py
+++ b/ctrack/organisations/admin.py
@@ -1,6 +1,15 @@
from django.contrib import admin
-from .models import Organisation, Address, AddressType, Person, Role, Mode, Submode
+from .models import (
+ Address,
+ AddressType,
+ Mode,
+ Organisation,
+ Person,
+ Role,
+ Stakeholder,
+ Submode,
+)
# So we can get the organisation name - a reverse lookup
@@ -16,6 +25,10 @@ class AddressTypeAdmin(admin.ModelAdmin):
pass
+class StakeholderAdmin(admin.ModelAdmin):
+ model = Stakeholder
+
+
class AddressInLine(admin.StackedInline):
model = Address
max_num = 3
@@ -24,7 +37,7 @@ class AddressInLine(admin.StackedInline):
class OrganisationAdmin(admin.ModelAdmin):
inlines = [AddressInLine]
- list_display = ("name", "submode", "date_updated", "updated_by")
+ list_display = ("name", "submode", "date_updated")
class PersonAdmin(admin.ModelAdmin):
@@ -58,3 +71,4 @@ admin.site.register(Role, RoleAdmin)
admin.site.register(Person, PersonAdmin)
admin.site.register(Mode, ModeAdmin)
admin.site.register(Submode, SubmodeAdmin)
+admin.site.register(Stakeholder, StakeholderAdmin)
diff --git a/ctrack/organisations/migrations/0005_auto_20200525_1502.py b/ctrack/organisations/migrations/0005_auto_20200525_1502.py
new file mode 100644
index 0000000..921fdbb
--- /dev/null
+++ b/ctrack/organisations/migrations/0005_auto_20200525_1502.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.0.5 on 2020-05-25 15:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('organisations', '0004_auto_20200513_1441'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='organisation',
+ name='updated_by',
+ ),
+ migrations.RemoveField(
+ model_name='person',
+ name='updated_by',
+ ),
+ migrations.CreateModel(
+ name='Stakeholder',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organisations.Person')),
+ ],
+ ),
+ ]
diff --git a/ctrack/organisations/models.py b/ctrack/organisations/models.py
index b11e97b..6a1b4ac 100644
--- a/ctrack/organisations/models.py
+++ b/ctrack/organisations/models.py
@@ -47,7 +47,7 @@ class Person(models.Model):
to handle when Users are deleted from the system, preventing the Person objects
related to them being deleted also.
"""
- return get_user_model().objects.get_or_create(username='DELETED USER')[0]
+ return get_user_model().objects.get_or_create(username="DELETED USER")[0]
primary_nis_contact = models.BooleanField(
default=False, verbose_name="Primary NIS contact"
@@ -65,7 +65,9 @@ class Person(models.Model):
mobile = models.CharField(max_length=20, blank=True)
landline = models.CharField(max_length=20, blank=True)
date_updated = models.DateField(auto_now=True)
- updated_by = models.ForeignKey(get_user_model(), on_delete=models.SET(get_sentinel_user))
+ # updated_by = models.ForeignKey(
+ # get_user_model(), on_delete=models.SET(get_sentinel_user)
+ # )
clearance = models.IntegerField(choices=CLEARANCE_LEVEL, default=1)
clearance_sponsor = models.CharField(max_length=100, blank=True)
clearance_start_date = models.DateField(blank=True, null=True)
@@ -74,7 +76,11 @@ class Person(models.Model):
active = models.BooleanField(default=True)
date_ended = models.DateField(blank=True, null=True)
predecessor = models.ForeignKey(
- "self", blank=True, on_delete=models.CASCADE, related_name="previous_person", null=True
+ "self",
+ blank=True,
+ on_delete=models.CASCADE,
+ related_name="previous_person",
+ null=True,
)
comments = models.TextField(max_length=1000, blank=True)
@@ -116,7 +122,7 @@ class Organisation(models.Model):
to handle when Users are deleted from the system, preventing the Organisations
related to them being deleted also.
"""
- return get_user_model().objects.get_or_create(username='DELETED USER')[0]
+ return get_user_model().objects.get_or_create(username="DELETED USER")[0]
name = models.CharField(max_length=255)
slug = AutoSlugField(populate_from=["name"])
@@ -128,7 +134,9 @@ class Organisation(models.Model):
registered_company_name = models.CharField(max_length=255, blank=True)
registered_company_number = models.CharField(max_length=100, blank=True)
date_updated = models.DateField(auto_now=True)
- updated_by = models.ForeignKey(get_user_model(), on_delete=models.SET(get_sentinel_user))
+ # updated_by = models.ForeignKey(
+ # get_user_model(), on_delete=models.SET(get_sentinel_user)
+ # )
comments = models.TextField(max_length=500, blank=True, null=True)
active = models.BooleanField(default=True)
@@ -166,3 +174,10 @@ class Address(models.Model):
class Meta:
verbose_name_plural = "Addresses"
+
+
+class Stakeholder(models.Model):
+ person = models.ForeignKey(Person, on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f"{self.person.first_name} {self.person.last_name}"
diff --git a/ctrack/organisations/templates/organisations/organisation_detail.html b/ctrack/organisations/templates/organisations/organisation_detail.html
index daec5a9..2479f50 100644
--- a/ctrack/organisations/templates/organisations/organisation_detail.html
+++ b/ctrack/organisations/templates/organisations/organisation_detail.html
@@ -48,7 +48,7 @@
</tr>
<tr>
<td><strong>Updated By:</strong></td>
- <td>{{ object.updated_by }}</td>
+ <td>REMOVED</td>
</tr>
<tr>
<td><strong>Active:</strong></td>
diff --git a/ctrack/organisations/tests/factories.py b/ctrack/organisations/tests/factories.py
index 7acd887..ba2655e 100644
--- a/ctrack/organisations/tests/factories.py
+++ b/ctrack/organisations/tests/factories.py
@@ -33,6 +33,8 @@ def _random_submode():
class UserFactory(DjangoModelFactory):
+ # Better to create this using example in ctrack.users.tests.factories.
+ # Handles password generation correctly.
class Meta:
model = User
@@ -51,7 +53,6 @@ class OrganisationFactory(DjangoModelFactory):
registered_company_name = Faker("company")
registered_company_number = Faker("numerify", text="######")
date_updated = Faker("date_this_year", before_today=True)
- updated_by = SubFactory(UserFactory)
comments = Faker("paragraph", nb_sentences=3)
active = True
@@ -99,7 +100,6 @@ class PersonFactory(DjangoModelFactory):
mobile = Faker("cellphone_number", locale="en_GB")
landline = Faker("phone_number", locale="en_GB")
date_updated = factory.LazyFunction(datetime.now)
- updated_by = SubFactory(UserFactory)
clearance = factory.LazyFunction(lambda: random.randint(1, 6))
clearance_sponsor = Faker("name", locale="en_GB")
clearance_start_date = factory.LazyFunction(datetime.now)
diff --git a/ctrack/organisations/views.py b/ctrack/organisations/views.py
index fe88728..b929de4 100644
--- a/ctrack/organisations/views.py
+++ b/ctrack/organisations/views.py
@@ -1,12 +1,15 @@
-from typing import Any
-from typing import Dict
+from typing import Any, Dict
-from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.mixins import (
+ LoginRequiredMixin,
+ PermissionRequiredMixin,
+ UserPassesTestMixin,
+)
from django.db import transaction
from django.urls import reverse_lazy
-from django.views.generic import DetailView, ListView, CreateView
+from django.views.generic import CreateView, DetailView, ListView
-from .forms import OrganisationCreateForm, AddressInlineFormSet
+from .forms import AddressInlineFormSet, OrganisationCreateForm
from .models import Organisation
@@ -27,19 +30,20 @@ class OrganisationCreate(LoginRequiredMixin, CreateView):
context = self.get_context_data()
addresses = context["addresses"]
with transaction.atomic():
- form.instance.updated_by = self.request.user
+ # form.instance.updated_by = self.request.user REMOVED updated_by
self.object = form.save()
if addresses.is_valid():
addresses.instance = self.object
addresses.save()
return super().form_valid(form)
- def get_success_url(self) -> str:
+ def get_success_url(self):
return reverse_lazy("organisations:detail", kwargs={"slug": self.object.slug})
-class OrganisationListView(LoginRequiredMixin, ListView):
+class OrganisationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Organisation
+ permission_required = "organisations.view_organisation"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -52,18 +56,18 @@ class OrganisationDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data()
- org = kwargs['object']
+ org = kwargs["object"]
no_addr = org.addresses.count()
if no_addr > 1:
- context['no_addr'] = no_addr
+ context["no_addr"] = no_addr
addr = org.addresses.all()
- context['addr'] = addr
+ context["addr"] = addr
else:
- context['no_addr'] = 1
+ context["no_addr"] = 1
addr = org.addresses.first()
- context['addr'] = addr
+ context["addr"] = addr
people = org.person_set.all()
- context['people'] = people
+ context["people"] = people
applicable_systems = org.applicablesystem_set.all()
- context['applicable_systems'] = applicable_systems
+ context["applicable_systems"] = applicable_systems
return context
diff --git a/ctrack/templates/403.html b/ctrack/templates/403.html
index 77db8ae..e722dd3 100644
--- a/ctrack/templates/403.html
+++ b/ctrack/templates/403.html
@@ -5,5 +5,7 @@
{% block content %}
<h1>Forbidden (403)</h1>
+<p>Sorry. You do not have permission to view this page.</p>
+
<p>CSRF verification failed. Request aborted.</p>
{% endblock content %}
diff --git a/ctrack/templates/account/login.html b/ctrack/templates/account/login.html
index bb261f4..f395478 100644
--- a/ctrack/templates/account/login.html
+++ b/ctrack/templates/account/login.html
@@ -38,7 +38,7 @@ for a {{ site_name }} account and sign in below:{% endblocktrans %}</p>
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<a class="button secondaryAction" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
- <button class="primaryAction btn btn-primary" type="submit">{% trans "Sign In" %}</button>
+ <button class="primaryAction btn btn-primary" id="sign_in_button" type="submit">{% trans "Sign In" %}</button>
</form>
{% endblock %}
diff --git a/ctrack/templates/base.html b/ctrack/templates/base.html
index 4398926..c0ee4cd 100644
--- a/ctrack/templates/base.html
+++ b/ctrack/templates/base.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
- <title>{% block title %}ctrack{% endblock title %}</title>
+ <title>{% block title %}ctrack - Department for Transport{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
diff --git a/ctrack/templates/base_stakeholder.html b/ctrack/templates/base_stakeholder.html
new file mode 100644
index 0000000..c2e8cbe
--- /dev/null
+++ b/ctrack/templates/base_stakeholder.html
@@ -0,0 +1,115 @@
+{% load static i18n %}<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <title>{% block title %}ctrack - Department for Transport{% endblock title %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
+ <!--[if lt IE 9]>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
+ <![endif]-->
+
+ <link rel="icon" href="{% static 'images/favicons/favicon.ico' %}">
+
+ {% block css %}
+
+ <!-- Latest compiled and minified Bootstrap CSS -->
+ <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+
+
+ <!-- Your stuff: Third-party CSS libraries go here -->
+
+ <!-- This file stores project-specific CSS -->
+
+ <link href="{% static 'css/project.css' %}" rel="stylesheet">
+{# <link rel="stylesheet" type="text/css" href="css/DataTables/datatables.min.css"/>#}
+ <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.10.20/af-2.3.4/b-1.6.1/b-print-1.6.1/cr-1.5.2/r-2.2.3/datatables.min.css"/>
+
+ {% endblock %}
+
+ </head>
+
+ <body>
+
+ <div class="mb-1">
+ <nav class="navbar lemon-bar navbar-expand-md navbar-light bg-light">
+ <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <a id="_title" class="navbar-brand" href="{% url 'core:home' %}">ctrack</a>
+
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
+ <ul class="navbar-nav mr-auto">
+ <li class="nav-item active">
+ <a class="nav-link" href="{% url 'core:home' %}">Home <span class="sr-only">(current)</span></a>
+ </li>
+
+ {% if request.user.is_authenticated %}
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a class="nav-link" href="{% url 'users:detail' request.user.username %}">{% trans "My Profile" %}</a>
+ </li>
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a class="nav-link" href="{% url 'account_logout' %}">{% trans "Sign Out" %}</a>
+ </li>
+ {% else %}
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
+ </li>
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+ </nav>
+
+ </div>
+
+ <div class="container">
+
+ {% if messages %}
+ {% for message in messages %}
+ <div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">{{ message }}<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>
+ {% endfor %}
+ {% endif %}
+
+ {% block content %}
+ <p>Use this document as a way to quick start any new project.</p>
+ {% endblock content %}
+
+ </div> <!-- /container -->
+
+ {% block modal %}{% endblock modal %}
+
+ <!-- Le javascript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ {% block javascript %}
+
+ <!-- Bootstrap JS and its dependencies-->
+ <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+
+ <!-- Your stuff: Third-party javascript libraries go here -->
+
+
+ <!-- place project specific Javascript in this file -->
+
+ <script src="{% static 'js/project.js' %}"></script>
+{# <script src="{% static 'js/DataTables/datatables.min.js' %}"></script>#}
+ <script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.10.20/af-2.3.4/b-1.6.1/b-print-1.6.1/cr-1.5.2/r-2.2.3/datatables.min.js"></script>
+
+
+ {% endblock javascript %}
+ </body>
+</html>
+
diff --git a/ctrack/templates/pages/home.html b/ctrack/templates/pages/home.html
index fbce9ff..324da8e 100644
--- a/ctrack/templates/pages/home.html
+++ b/ctrack/templates/pages/home.html
@@ -2,8 +2,6 @@
{% block content %}
- <h1>User profile page</h1>
-
- <p>User: {{ object }}</p>
+ <p>THIS IS A TEMPLATE FOR A REGULAR USER</p>
{% endblock content %}
diff --git a/ctrack/templates/pages/stakeholder_home.html b/ctrack/templates/pages/stakeholder_home.html
new file mode 100644
index 0000000..fd40c7a
--- /dev/null
+++ b/ctrack/templates/pages/stakeholder_home.html
@@ -0,0 +1,162 @@
+{% extends "base_stakeholder.html" %}
+
+{% load static %}
+
+{% block content %}
+
+ <div class="container mt-3">
+ <div class="row">
+ <div class="col-md-12 pl-0 my-2">
+
+ <h2>{{ org }} <span class="badge badge-light">{{ org.submode }}</span></h2>
+
+ <p><span class="text-muted">{{ org.person_set.first }}</span></p>
+
+ <p>THIS IS A TEMPLATE FOR A STAKEHOLDER USER</p>
+
+ <div class="row">
+ <div class="col-12">
+ <div class="border border-success p-2 rounded bg-light">
+ <h5>KEY INFORMATION:</h5>
+ <p>Please note that important notices go in here. This is the NIS Directive
+ portal for DfT. Etc.</p>
+ <p>Your lead inspector is <a href="#">Bob McKinnon</a>.</p>
+ <p>Other important messages will appear here when we deem it necessary.
+ Please ensure you remain in touch with what appears in this box because it
+ <strong>WILL</strong> be updated periodically.</p>
+ <span class="text-muted"><a href="#">Help</a> |
+ <a href="#">NIS Regulations</a> |
+ <a href="#">Improvement Plan Information</a> |
+ <a href="#">Report a problem</a></span>
+ </div>
+ </div>
+ </div>
+
+ <hr>
+
+ <div class="row">
+ <div class="col md-12">
+ <h2>Incident Reporting</h2>
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col">Incident</th>
+ <th scope="col">Date</th>
+ <th scope="col">Details</th>
+ <th scope="col">Status</th>
+ </tr>
+ </thead>
+ <tr>
+ <td>Power failure at Random Site</td>
+ <td>12 May 2020</td>
+ <td>There was a problem with some wires inside the black box at the site.
+ There was very little we could do until someone switched the circuit breakers off,
+ then we had to switch off the server that supplies the TLS link to the estate management
+ system. We failed on this one, big time.</td>
+ <td><strong><span class="badge badge-warning">UNRESOLVED</span></strong></td>
+ </tr>
+ <tr>
+ <td>Corruption of main database</td>
+ <td>8 December 2019</td>
+ <td>Weather got to the night-watchperson, who typed in the command to seal
+ the compound incorrectly. This led to a deluge of sand into the minky processor
+ which eventually wiped all data tables.</td>
+ <td><strong><span class="badge badge-success">RESOLVED</span></strong></td>
+ </tr>
+ </table>
+ <button class="btn btn-primary">Report a NIS incident</button>
+ </div>
+ </div>
+ <hr>
+ <div class="row">
+ <div class="col md-12">
+ <h2>Compliance Events</h2>
+ <table class="table">
+ <thead>
+ <th scope="col">Date</th>
+ <th scope="col">Type</th>
+ <th scope="col">Location</th>
+ <th scope="col">Notes</th>
+ <th scope="col">CAF scope</th>
+ <th scope="col">Status</th>
+ <th scope="col">Report</th>
+ </thead>
+ <tr>
+ <td>23 October 2020</td>
+ <td>Inspection</td>
+ <td>Hammersmith</td>
+ <td>Follow up to <a href="#">July call</a></td>
+ <td>
+ <span class="badge badge-warning">A2.c</span>
+ <span class="badge badge-danger">B2.a</span>
+ </td>
+ <td><span class="badge badge-warning">SCHEDULED</span></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>5 June 2020</td>
+ <td>Audit</td>
+ <td>Hammersmith</td>
+ <td>NA</td>
+ <td>
+ <span class="badge badge-danger">A2.c</span>
+ <span class="badge badge-secondary">B5.c</span>
+ <span class="badge badge-secondary">B6</span>
+ <span class="badge badge-secondary">B7</span>
+ </td>
+ <td><span class="badge badge-success">RESOLVED</span></td>
+ <td><a href="#">DfT-NIS-2302A</a></td>
+ <tr>
+ <tr>
+ <td>19 January 2020</td>
+ <td>Meeting</td>
+ <td>Video</td>
+ <td>NA</td>
+ <td>
+ <span class="badge badge-secondary">Obj C</span>
+ </td>
+ <td><span class="badge badge-success">RESOLVED</span></td>
+ <td></td>
+ <tr>
+ </table>
+ </div>
+ </div>
+
+ <hr>
+
+ <div class="row">
+ <div class="col md-12">
+ <h2>NIS systems</h2>
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col">System Name</th>
+ <th scope="col">Description</th>
+ <th scope="col">CAF</th>
+ </tr>
+ </thead>
+ {% for system in systems %}
+ <tr>
+ <td>{{ system.name }}</td>
+ <td>{{ system.description }}</td>
+ <td>{{ system.caf }} | <small><a href="#">Update WebCAF</a></small></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ </div>
+
+ <hr>
+
+ <div class="row">
+ <div class="col md-12">
+ <h2>DfT Engagement</h2>
+ <p>No engagement with DfT currently scheduled</p>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+{% endblock content %}
diff --git a/ctrack/users/admin.py b/ctrack/users/admin.py
index 120cc64..7e930d8 100644
--- a/ctrack/users/admin.py
+++ b/ctrack/users/admin.py
@@ -12,6 +12,8 @@ class UserAdmin(auth_admin.UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
- fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets
- list_display = ["username", "name", "is_superuser"]
+ fieldsets = (
+ ("User", {"fields": ("name", "stakeholder")}),
+ ) + auth_admin.UserAdmin.fieldsets
+ list_display = ["username", "name", "is_superuser", "stakeholder"]
search_fields = ["name"]
diff --git a/ctrack/users/migrations/0005_delete_userprofile.py b/ctrack/users/migrations/0005_delete_userprofile.py
new file mode 100644
index 0000000..42f62bb
--- /dev/null
+++ b/ctrack/users/migrations/0005_delete_userprofile.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.0.5 on 2020-05-25 14:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0004_auto_20200524_1945'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='UserProfile',
+ ),
+ ]
diff --git a/ctrack/users/migrations/0006_user_stakeholder.py b/ctrack/users/migrations/0006_user_stakeholder.py
new file mode 100644
index 0000000..d8a3089
--- /dev/null
+++ b/ctrack/users/migrations/0006_user_stakeholder.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.0.5 on 2020-05-25 15:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('organisations', '0005_auto_20200525_1502'),
+ ('users', '0005_delete_userprofile'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='stakeholder',
+ field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organisations.Stakeholder'),
+ ),
+ ]
diff --git a/ctrack/users/models.py b/ctrack/users/models.py
index 8f07b15..052efd6 100644
--- a/ctrack/users/models.py
+++ b/ctrack/users/models.py
@@ -1,14 +1,28 @@
from django.contrib.auth.models import AbstractUser
-from django.db.models import CharField
+from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
+from ctrack.organisations.models import Stakeholder
+
class User(AbstractUser):
- # First Name and Last Name do not cover name patterns
- # around the globe.
- name = CharField(_("Name of User"), blank=True, max_length=255)
+ name = models.CharField(_("Name of User"), blank=True, max_length=255)
+ stakeholder = models.OneToOneField(
+ Stakeholder, on_delete=models.CASCADE, null=True, blank=True
+ )
def get_absolute_url(self):
return reverse("users:detail", kwargs={"username": self.username})
+
+ @property
+ def is_stakeholder(self):
+ if self.stakeholder is not None:
+ return True
+ else:
+ return False
+
+ def get_organisation_name(self):
+ if self.is_stakeholder:
+ return self.stakeholder.person.organisation.name
diff --git a/ctrack/users/stakeholder.py b/ctrack/users/stakeholder.py
index d15dd39..a8b4329 100644
--- a/ctrack/users/stakeholder.py
+++ b/ctrack/users/stakeholder.py
@@ -1,7 +1,3 @@
from django.db import models
from ctrack.organisations.models import Person
-
-
-class Stakeholder(models.Model):
- person = models.ForeignKey(Person, on_delete=models.CASCADE)
diff --git a/ctrack/users/tests/test_functional.py b/ctrack/users/tests/test_functional.py
new file mode 100644
index 0000000..5622ab3
--- /dev/null
+++ b/ctrack/users/tests/test_functional.py
@@ -0,0 +1,124 @@
+"""
+Functional tests. Are probably SLOW thanks to using Selenium to load a browser instance.
+
+The use case being tested here is related to a user being able to log in and hit
+the correct page, containing their details. Those details depend on whether they are
+a regular user or a stakeholder user.
+"""
+
+import time
+
+import pytest
+from django.contrib.auth.models import Permission
+
+from ctrack.users.models import User
+
+pytestmark = pytest.mark.django_db
+
+
+def test_regular_user_can_log_in(browser, live_server):
+
+ # Toss McBride is an OES user. He logs into the system...
+ User.objects.create_user(username="toss", password="knob")
+ browser.get(live_server + "/accounts/login")
+ browser.find_element_by_id("id_login").send_keys("toss")
+ browser.find_element_by_id("id_password").send_keys("knob")
+ browser.find_element_by_id("sign_in_button").submit()
+ time.sleep(1)
+ current_url = browser.current_url
+ assert current_url == live_server + "/"
+
+ type_user_message = browser.find_elements_by_tag_name("p")
+ assert "THIS IS A TEMPLATE FOR A REGULAR USER" in [
+ m.text for m in type_user_message
+ ]
+
+
+def test_stakeholder_can_log_in_and_see_their_home(browser, live_server, stakeholder):
+ # Toss McBride is an OES user. He logs into the system...
+
+ user = User.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ org = user.stakeholder.person.get_organisation_name()
+ user.save()
+ browser.get(live_server + "/accounts/login")
+ browser.find_element_by_id("id_login").send_keys("toss")
+ browser.find_element_by_id("id_password").send_keys("knob")
+ browser.find_element_by_id("sign_in_button").submit()
+ time.sleep(1)
+ current_url = browser.current_url
+ assert current_url == live_server + "/"
+
+ p_tags = browser.find_elements_by_tag_name("p")
+ h2_tags = browser.find_elements_by_tag_name("h2")
+ assert "THIS IS A TEMPLATE FOR A STAKEHOLDER USER" in [m.text for m in p_tags]
+ assert org in [m.text for m in h2_tags]
+ assert (
+ f"{user.stakeholder.person.first_name} {user.stakeholder.person.last_name}"
+ in [m.text for m in p_tags]
+ )
+
+
+def test_stakeholder_can_log_in_but_receieved_permisson_denied_when_off_piste(
+ browser, live_server, stakeholder
+):
+ user = User.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ browser.get(live_server + "/accounts/login")
+ browser.find_element_by_id("id_login").send_keys("toss")
+ browser.find_element_by_id("id_password").send_keys("knob")
+ browser.find_element_by_id("sign_in_button").submit()
+ time.sleep(1)
+ # Try to browser to Organisations list
+ browser.get(live_server + "/organisations")
+ assert "Sorry. You do not have permission to view this page." in [
+ x.text for x in browser.find_elements_by_tag_name("p")
+ ]
+
+
+def test_stakeholder_user_with_permissions_can_view_page(
+ browser, live_server, stakeholder
+):
+ user = User.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ org_list_permission = Permission.objects.get(name="Can view organisation")
+
+ # Add the permission to view an Organisation, which is set on OrganisationListView
+ assert user.user_permissions.count() == 0
+ user.user_permissions.add(org_list_permission)
+ assert user.user_permissions.count() == 1
+ user.save()
+
+ browser.get(live_server + "/accounts/login")
+ browser.find_element_by_id("id_login").send_keys("toss")
+ browser.find_element_by_id("id_password").send_keys("knob")
+ browser.find_element_by_id("sign_in_button").submit()
+ time.sleep(1)
+ # Try to browser to Organisations list
+ browser.get(live_server + "/organisations")
+ assert "Organisations" in browser.title
+
+
+def test_stakeholder_user_can_see_requisite_subtitles_on_home_page(
+ browser, live_server, stakeholder
+):
+ user = User.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ browser.get(live_server + "/accounts/login")
+ browser.find_element_by_id("id_login").send_keys("toss")
+ browser.find_element_by_id("id_password").send_keys("knob")
+ browser.find_element_by_id("sign_in_button").submit()
+ time.sleep(1)
+ current_url = browser.current_url
+ assert current_url == live_server + "/"
+
+ # On the other side, he sees some basic details about himself.
+ assert "ctrack - Department for Transport" in browser.title
+
+ h2 = browser.find_elements_by_tag_name("h2")
+ assert "Incident Reporting" in [x.text for x in h2]
+ assert "Audits and Inspections" in [x.text for x in h2]
+ assert "NIS systems" in [x.text for x in h2]
+ assert "DfT Engagement" in [x.text for x in h2]
diff --git a/ctrack/users/tests/test_models.py b/ctrack/users/tests/test_models.py
index 2d45cca..368be34 100644
--- a/ctrack/users/tests/test_models.py
+++ b/ctrack/users/tests/test_models.py
@@ -1,6 +1,6 @@
import pytest
-from ctrack.users.stakeholder import Stakeholder
+from ctrack.organisations.models import Stakeholder
pytestmark = pytest.mark.django_db
@@ -16,10 +16,14 @@ def test_user_is_person_object(user):
assert user
-def test_stakeholder_model(person):
+def test_stakeholder_model(person, user):
"""
A stakeholder is someone who is part of the regime but also has user access to the
the system.
"""
stakeholder = Stakeholder(person=person)
- assert stakeholder
+ org = person.organisation.name
+ user.stakeholder = stakeholder
+ assert user.stakeholder.person.first_name == "Toss"
+ assert user.is_stakeholder is True
+ assert user.get_organisation_name() == org
diff --git a/ctrack/users/tests/test_views.py b/ctrack/users/tests/test_views.py
index 3299cd6..8dc4825 100644
--- a/ctrack/users/tests/test_views.py
+++ b/ctrack/users/tests/test_views.py
@@ -1,7 +1,9 @@
import pytest
-from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
from django.test import RequestFactory
+from ctrack.core.views import home_page
+from ctrack.organisations.views import OrganisationListView
from ctrack.users.models import User
from ctrack.users.views import UserDetailView, UserRedirectView, UserUpdateView
@@ -44,23 +46,137 @@ class TestUserRedirectView:
view.request = request
- assert view.get_redirect_url() == f"/users/{user.username}/"
+ assert view.get_redirect_url() == "/"
-def test_profile_view_contains_organisation_information():
- """url: users/username
- This is where users are redirected to when they log in and where I want to capture
- information about the user - particularly if they are an OES user.
+def test_profile_view_contains_organisation_information(
+ person, user, request_factory, stakeholder
+):
"""
- user = get_user_model().objects.create_user(
- username="testy", email="testy@test.com", password="test1020"
- )
- factory = RequestFactory()
- request = factory.get(f"/users/{user.username}")
+ This tests the context_data - not the rendered page... We'll do that in the
+ next test.
+ """
+ org_name = person.organisation.name
+ user.stakeholder = stakeholder
+ user.save()
+ request = request_factory.get(f"/users/{user.username}")
+
# we have to do the following to simulate logged-in user
# Django Advanced Testing Topics
request.user = user
+
+ # We pass 'username' rather than 'slug' here because we are setting 'slug_url_kwarg' in our CBV.
response = UserDetailView.as_view()(request, username=user.username)
+
+ assert response.status_code == 200
+ assert response.context_data["user"].username == user.username
+ assert response.context_data["user"].is_stakeholder is True
+ assert response.context_data["user"].stakeholder.person.first_name == "Toss"
+
+ # Two ways of getting the organisaton name
+ assert (
+ response.context_data["user"].stakeholder.person.get_organisation_name()
+ == org_name
+ )
+ assert response.context_data["user"].get_organisation_name() == org_name
+ assert response.context_data["user"].stakeholder.person.first_name == "Toss"
+
+
+def test_home_page_h1_tag_with_client(client, django_user_model):
+ """
+ Basic test of HTML from the home page.
+ """
+ django_user_model.objects.create_user(username="toss", password="knob")
+ client.login(username="toss", password="knob")
+ response = client.get("/")
+ assert response.status_code == 200
+ assert response.content[:15] == b"<!DOCTYPE html>"
+ assert b"<title>ctrack - Department for Transport</title>" in response.content
+ assert b"<h1>Welcome to ctrack - Department for Transport</h1>" in response.content
+ assert b"</html>" in response.content
+
+
+def test_regular_user_redirected_to_their_template_on_login(
+ django_user_model, request_factory: RequestFactory
+):
+ """
+ When a user logs in without a stakeholder mapping, they get sent to the regular user
+ template.
+ """
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ request = request_factory.get("/")
+ request.user = user
+ response = home_page(request)
+ assert response.status_code == 200
+ assert b"<p>THIS IS A TEMPLATE FOR A REGULAR USER</p>" in response.content
+
+
+def test_stakeholder_redirected_to_their_template_on_login(
+ django_user_model, request_factory: RequestFactory, stakeholder
+):
+ """
+ When a user logs in WITH a stakeholder mapping, they get sent to the stakehoder user
+ template.
+ """
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ request = request_factory.get("/")
+ request.user = user
+ response = home_page(request)
+ assert response.status_code == 200
+ assert b"THIS IS A TEMPLATE FOR A STAKEHOLDER USER" in response.content
+
+
+def test_stakeholder_returns_is_stakeholder(
+ django_user_model, request_factory, stakeholder
+):
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ request = request_factory.get("/")
+ request.user = user
+ assert request.user.is_stakeholder is True
+
+
+def test_stakeholder_user_is_not_staff(django_user_model, stakeholder):
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ assert user.is_staff is False
+
+
+def test_stakeholder_user_gets_301_when_trying_to_access_view_with_perm_set(
+ django_user_model, client, stakeholder
+):
+ """
+ No permissions are set when a regular user is created. This test knows that a suitable
+ permission is set on the ctrack.organisations.view.OrganisationListView, and therefore we
+ would expect a redirect/403 persmission denied response when trying to reach it with a
+ regular user.
+ """
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ user.save()
+ client.login(username="toss", password="knob")
+ response = client.get(path="https://localhost:8000/organisations")
+ assert (
+ response.status_code == 301
+ ) # This page redirects to 403.html, hence why its a 301 (I think)
+
+
+@pytest.mark.skip("Explore why this does not pass - it passess in functional style")
+def test_staff_user_gets_200_when_trying_to_access_view_with_perm_set(
+ django_user_model, client, stakeholder
+):
+ user = django_user_model.objects.create_user(username="toss", password="knob")
+ user.stakeholder = stakeholder
+ org_list_permission = Permission.objects.get(name="Can view organisation")
+ assert user.user_permissions.count() == 0
+ user.user_permissions.add(org_list_permission)
+ assert user.has_perm("organisations.view_organisation")
+ user.save()
+ logged_in = client.login(username="toss", password="knob")
+ assert logged_in is True
+ response = client.get("/organisations")
assert response.status_code == 200
- # TODO - work out how we can attach an organisation to the User model
- assert False, "This does nothing yet"
diff --git a/ctrack/users/views.py b/ctrack/users/views.py
index a292191..8e504e4 100644
--- a/ctrack/users/views.py
+++ b/ctrack/users/views.py
@@ -11,7 +11,13 @@ User = get_user_model()
class UserDetailView(LoginRequiredMixin, DetailView):
model = User
+
+ # This names the field in the model that contains the slug. Want it to be thise so that is a good
+ # citizen to be used in a URL
slug_field = "username"
+
+ # the name of the URLConf keyword argument that contains the slug. By default, slug_url_kwarg is 'slug'.
+ # we have to pass 'username' as the argument when testing UserDetailView because of this.
slug_url_kwarg = "username"
@@ -44,7 +50,11 @@ class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False
def get_redirect_url(self):
- return reverse("users:detail", kwargs={"username": self.request.user.username})
+ return reverse("core:home")
+
+
+# def get_redirect_url(self):
+# return reverse("users:detail", kwargs={"username": self.request.user.username})
user_redirect_view = UserRedirectView.as_view()