diff options
author | Matthew Lemon <lemon@matthewlemon.com> | 2020-05-28 16:12:13 +0100 |
---|---|---|
committer | Matthew Lemon <lemon@matthewlemon.com> | 2020-05-28 16:12:13 +0100 |
commit | acf875c2bca1727306cb70aaa37e6f4595209786 (patch) | |
tree | 67390297bcd02b2481f5673629227dfea0884215 /ctrack | |
parent | 4aaa68109434e04d1103710d675efd075b2f744a (diff) | |
parent | 520a9deb13f93c689443c0339b1c96a67f0c15a1 (diff) |
Merge branch 'stakeholder-profile'
Diffstat (limited to '')
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">×</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() |