From 8f1672a8e04891829f143aad65603cecc2b351d3 Mon Sep 17 00:00:00 2001 From: Virginia Dooley Date: Fri, 5 Mar 2021 11:57:24 +0000 Subject: [PATCH] Apply design system and refactor new person page --- requirements/base.txt | 1 + .../migrations/0003_auto_20210225_1103.py | 17 + .../migrations/0003_auto_20210225_1103.py | 17 + .../migrations/0003_auto_20210225_1103.py | 17 + wcivf/apps/parties/models.py | 4 + wcivf/apps/people/models.py | 68 ++++ .../people/includes/_person_about_card.html | 40 ++ .../people/includes/_person_contact_card.html | 108 ++++++ .../includes/_person_edit_details_card.html | 42 ++ .../includes/_person_hustings_card.html | 14 + .../people/includes/_person_intro_card.html | 33 ++ .../people/includes/_person_policy_card.html | 106 +++++ .../_person_previous_elections_card.html | 34 ++ .../people/not_current_person_detail.html | 47 ++- .../templates/people/person_detail.html | 367 ++---------------- wcivf/apps/people/tests/test_person_models.py | 71 ++++ wcivf/apps/people/tests/test_person_views.py | 218 ++++++++++- wcivf/apps/people/views.py | 15 +- wcivf/assets/scss/main.scss | 71 +++- wcivf/settings/base.py | 6 + 20 files changed, 924 insertions(+), 372 deletions(-) create mode 100644 wcivf/apps/core/migrations/0003_auto_20210225_1103.py create mode 100644 wcivf/apps/feedback/migrations/0003_auto_20210225_1103.py create mode 100644 wcivf/apps/news_mentions/migrations/0003_auto_20210225_1103.py create mode 100644 wcivf/apps/people/templates/people/includes/_person_about_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_contact_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_edit_details_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_hustings_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_intro_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_policy_card.html create mode 100644 wcivf/apps/people/templates/people/includes/_person_previous_elections_card.html create mode 100644 wcivf/apps/people/tests/test_person_models.py diff --git a/requirements/base.txt b/requirements/base.txt index e71b6831e..b31a56d19 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -21,6 +21,7 @@ newspaper3k==0.2.8 git+git://github.com/DemocracyClub/dc_base_theme.git@0.3.11 git+https://github.com/DemocracyClub/dc_signup_form.git@2.1.0 +git+https://github.com/DemocracyClub/design-system.git djangorestframework-jsonp==1.0.2 feedparser==6.0.2 diff --git a/wcivf/apps/core/migrations/0003_auto_20210225_1103.py b/wcivf/apps/core/migrations/0003_auto_20210225_1103.py new file mode 100644 index 000000000..0e1b1d007 --- /dev/null +++ b/wcivf/apps/core/migrations/0003_auto_20210225_1103.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.18 on 2021-02-25 11:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0002_auto_20170526_1448"), + ] + + operations = [ + migrations.AlterModelOptions( + name="loggedpostcode", + options={"get_latest_by": "modified"}, + ), + ] diff --git a/wcivf/apps/feedback/migrations/0003_auto_20210225_1103.py b/wcivf/apps/feedback/migrations/0003_auto_20210225_1103.py new file mode 100644 index 000000000..3a5c1108c --- /dev/null +++ b/wcivf/apps/feedback/migrations/0003_auto_20210225_1103.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.18 on 2021-02-25 11:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("feedback", "0002_feedback_token"), + ] + + operations = [ + migrations.AlterModelOptions( + name="feedback", + options={"get_latest_by": "modified"}, + ), + ] diff --git a/wcivf/apps/news_mentions/migrations/0003_auto_20210225_1103.py b/wcivf/apps/news_mentions/migrations/0003_auto_20210225_1103.py new file mode 100644 index 000000000..15d3da34c --- /dev/null +++ b/wcivf/apps/news_mentions/migrations/0003_auto_20210225_1103.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.18 on 2021-02-25 11:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("news_mentions", "0002_ballotnewsarticle_publisher"), + ] + + operations = [ + migrations.AlterModelOptions( + name="ballotnewsarticle", + options={"get_latest_by": "modified"}, + ), + ] diff --git a/wcivf/apps/parties/models.py b/wcivf/apps/parties/models.py index f8f83103b..5921ed516 100644 --- a/wcivf/apps/parties/models.py +++ b/wcivf/apps/parties/models.py @@ -87,6 +87,10 @@ def is_independent(self): """ return self.party_id == "ynmp-party:2" + @property + def is_joint_party(self): + return self.party_id.startswith("joint-party:") + class LocalParty(models.Model): parent = models.ForeignKey( diff --git a/wcivf/apps/people/models.py b/wcivf/apps/people/models.py index 0ffe47112..11899c8df 100644 --- a/wcivf/apps/people/models.py +++ b/wcivf/apps/people/models.py @@ -153,6 +153,74 @@ def get_max_facebook_ad_spend(self): ) ) + @property + def facebook_personal_username(self): + facebook_personal_url = self.facebook_personal_url + facebook_split = list(filter(None, facebook_personal_url.split("/"))) + facebook_personal_username = facebook_split[-1] + + return facebook_personal_username + + @property + def facebook_username(self): + facebook_page_url = self.facebook_page_url + facebook_split = list(filter(None, facebook_page_url.split("/"))) + return facebook_split[-1] + + @property + def instagram_username(self): + instagram_url = self.instagram_url + instagram_split = list(filter(None, instagram_url.split("/"))) + instagram_username = instagram_split[-1] + + return instagram_username + + @property + def linkedin_username(self): + linkedin_url = self.linkedin_url + linkedin_split = list(filter(None, linkedin_url.split("/"))) + linkedin_username = linkedin_split[-1] + + return linkedin_username + + @property + def youtube_username(self): + youtube_url = self.youtube_profile + if "channel" in youtube_url: + return self.name + "'s Channel" + youtube_split = list(filter(None, youtube_url.split("/"))) + youtube_username = youtube_split[-1] + return youtube_username + + @property + def long_statement(self): + return self.statement_count > 100 + + @property + def statement_count(self): + statement = self.statement_to_voters + statement_count = len(statement.split()) + return statement_count + + @property + def statement_intro(self): + statement_intro = self.statement_to_voters.split(".")[0] + "." + return statement_intro + + @property + def statement_remainder(self): + statement_split = self.statement_to_voters.split(".") + statement_remainder = ".".join(statement_split[1:]) + + return statement_remainder + + @property + def display_deceased(self): + if self.death_date and self.current_or_future_candidacies: + return True + else: + return False + @cached_property def current_or_future_candidacies(self): """ diff --git a/wcivf/apps/people/templates/people/includes/_person_about_card.html b/wcivf/apps/people/templates/people/includes/_person_about_card.html new file mode 100644 index 000000000..d93272896 --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_about_card.html @@ -0,0 +1,40 @@ +{% if object.wikipedia_bio or object.associatedcompany_set.exists or object.cv %} +
+
+

+ About {{ object.name }} +

+ {% if object.cv %} +

+

CV

+

{{ object.cv }}

+ {% endif %} +

+ + {% if object.wikipedia_bio %} +

Wikipedia

+

{{ object.wikipedia_bio }}

+ Read more on Wikipedia + {% endif %} + + {% if object.associatedcompany_set.exists %} +

Associated companies

+

{{object.name}} is associated with the following companies:

+
    + {% for company in object.associatedcompany_set.all %} +
  • + {{ company.company_name }} + + {{ company.role }}{% if company.role_status %}({{ company.role_status }}){% endif %} + {{ company.role_appointed_date | date:"Y" }} – {{ company.role_resigned_date | date:"Y"}} + + + More details from Companies House} + +
  • + {% endfor %} +
+ {% endif %} +
+
+{% endif %} diff --git a/wcivf/apps/people/templates/people/includes/_person_contact_card.html b/wcivf/apps/people/templates/people/includes/_person_contact_card.html new file mode 100644 index 000000000..dd177dc32 --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_contact_card.html @@ -0,0 +1,108 @@ +
+
+

+ {{ object.name }} online +

+
+ {% if object.facebook_page_url or object.facebook_personal_url %} +
+
Facebook
+
+ {% if object.facebook_personal_url %} +
+ + {{ object.facebook_personal_username }} +
+
+ {% endif %} + {% if object.facebook_page_url %} +
+ + {{ object.facebook_username}} + +
+ {% endif %} +
+
+ {% endif %} + + {% if object.linkedin_url %} +
+
LinkedIn
+
+ + {{ object.linkedin_username}} + +
+
+ {% endif %} + + {% if object.homepage_url %} +
+
Home page
+
+ + {{ object.homepage_url }} + +
+
+ {% endif %} + + {% if object.instagram_url %} +
+
Instagram
+
+ + {{ object.instagram_username }} + +
+
+ {% endif %} + + {% if object.youtube_profile %} +
+
YouTube
+
+ + {{ object.youtube_username }} + +
+
+ {% endif %} + + {% if object.party_ppc_page_url %} +
+
The party's candidate page for this person
+
+ + {{ object.party_ppc_page_url }} + +
+
+ {% endif %} + + {% if object.email and object.current_or_future_candidacies %} +
+
Email
+
+ {{ object.email }} +
+
+ {% else %} +
+
We don't know {{ object.name }}'s email address.
+
+ Can you add it? +
+
+ {% endif %} +
+

Latest tweets

+ +
+
+ diff --git a/wcivf/apps/people/templates/people/includes/_person_edit_details_card.html b/wcivf/apps/people/templates/people/includes/_person_edit_details_card.html new file mode 100644 index 000000000..af72dd1bb --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_edit_details_card.html @@ -0,0 +1,42 @@ +{% load humanize %} + +{% if object.current_or_future_candidacies %} +
+
+

That's all we know! Will you help us find more about this candidate?

+

Our volunteers have been working hard to add information on as many + candidates as possible, but they need help. +

+ + {% if object.should_show_email_cta %} +

Thousands of voters will rely on this site. + {% else %} +

+ {% endif %} + If you can add information that should be on this page + + {% if object.cta_example_details %} + - such as {{ object.name }}'s + {{ object.cta_example_details|join:", " }} - + {% endif %} + please use our crowdsourcing website to add it.

+ + Add or edit details » + + {% if object.should_show_email_cta %} +

You can also email {{ object.name }} directly to ask them to add information to this page. +

+ Ask the + candidate for more information » +

+ {% endif %} + +

Upload your leaflets

+

If you've received election leaflets from {{ object.name }}, please take a photo + of them and upload them to ElectionLeaflets.org

+

Add leaflets +

+
+
+{% endif %} diff --git a/wcivf/apps/people/templates/people/includes/_person_hustings_card.html b/wcivf/apps/people/templates/people/includes/_person_hustings_card.html new file mode 100644 index 000000000..326a0cb46 --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_hustings_card.html @@ -0,0 +1,14 @@ +{% load humanize %} + +{% if object.postelection.husting_set.exists and object.current_or_future_candidacies %} +
+
+

Local hustings

+

You can meet candidates and question them at local hustings. Here are hustings where {{ object.name }} may + be + appearing. +

+ {% include "elections/includes/_hustings_list.html" with hustings=object.postelection.husting_set.all %} +
+
+{% endif %} diff --git a/wcivf/apps/people/templates/people/includes/_person_intro_card.html b/wcivf/apps/people/templates/people/includes/_person_intro_card.html new file mode 100644 index 000000000..d4f647b7a --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_intro_card.html @@ -0,0 +1,33 @@ +
+
+

+ {{ object.name }} +

+ {% if person.display_deceased and person.personpost.party.is_independent %} + (Deceased) + {% endif %} + + {% if object.current_or_future_candidacies.all.count > 1 %} +
{{ object.name }} is {% if object.personpost.party.is_independent %}an{% else %}a{% endif %} {{object.personpost.party.party_name }} candidate in the following elections:
+ + {% for candidacy in object.current_or_future_candidacies %} +
    +
  • + {{ candidacy.election.name }} for {{ candidacy.post_election.friendly_name }} +
  • +
+ {% endfor %} + + {% else %} +

+ {{ object.intro|safe }}. +

+ {% endif %} +
+ + {% if object.photo_url %} +
+ profile photo of {{ object.name }} +
+ {% endif %} +
diff --git a/wcivf/apps/people/templates/people/includes/_person_policy_card.html b/wcivf/apps/people/templates/people/includes/_person_policy_card.html new file mode 100644 index 000000000..39a5a28c3 --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_policy_card.html @@ -0,0 +1,106 @@ +{% load humanize %} +{% load markdown_deux_tags %} + +{% if object.statement_to_voters or object.manifestos or object.leaflet_set.exists or object.twfy_id %} +
+
+

+ {{ object.name }}'s policies +

+ + {% if object.statement_to_voters %} +

Statement to voters

+ {% if object.long_statement %} +
    +
    +

    {{ object.statement_intro }}

    +
    + Full statement +

    {{ object.statement_remainder|linebreaksbr }}

    +
    +
    +
+ {% else %} +
{{ object.statement_to_voters|linebreaks }}
+ {% endif %} +

This statement was added by {{ object.name }}, their team, or by a Democracy Club volunteer, + based on + information published by the candidate elsewhere.

+ {% endif %} + + + {% if object.manifestos %} + {% with object.personpost.party as party %} + + {% if party.emblem %} + {{ party.name}} emblem + {{ party.name}} emblem + {{ party.name}} emblem + {% endif %} +

Party manifesto

+

Party manifesto

+

Party manifesto

+

+ {{ object.name }} {{ object.personpost.election.in_past|yesno:"was,is" }} the {{ party.party_name }} + candidate. + Find out more about their policies in the {{ party.party_name }} manifesto. +

+
    + {% for manifesto in object.manifestos %} + {% include "parties/single_manifesto.html" %} + {% endfor %} +
+ {% endwith %} + {% endif %} + + + {% if object.leaflet_set.exists %} +

+ Recent leaflets from {{ object.name }} +

+ {% for leaflet in object.leaflet_set.latest_four %} + {% if leaflet.thumb_url %} + } + Thumbnail of leaflet from {{ object.name }} + + {% endif %} +

+ Uploaded {{ leaflet.date_uploaded_to_electionleaflets|naturalday:"j M Y" }}
+ See leaflet +

+ {% endfor %} +

+ More leaflets from {{ + object.name }} + Upload a leaflet +

+ {% endif %} + + + {% if object.pledges.exists %} +

Candidate pledges

+

Hope Not Hate, a London-based charity, canvassed local residents and community organisations to establish + the + following questions to the candidates in this ward. +

+ {% for pledge in object.pledges.all %} +

{{ pledge.question }}

+
{{ pledge.answer|markdown }}
+ {% endfor %} + {% endif %} + + + {% if object.twfy_id %} +

Record in office

+

See this candidate's record on + TheyWorkForYou - + their speeches, voting history and more. +

+ {% endif %} +
+
+{% endif %} diff --git a/wcivf/apps/people/templates/people/includes/_person_previous_elections_card.html b/wcivf/apps/people/templates/people/includes/_person_previous_elections_card.html new file mode 100644 index 000000000..238b7b832 --- /dev/null +++ b/wcivf/apps/people/templates/people/includes/_person_previous_elections_card.html @@ -0,0 +1,34 @@ +{% load humanize %} + +{% if object.past_not_current_candidacies %} +
+
+ + + + + + + + + + {% for person_post in object.past_not_current_candidacies %} + {{ person_post.post_election.short_cancelled_message_html }} + {{ person_post.post_election.short_cancelled_message_html }} + {{ person_post.post_election.short_cancelled_message_html }} + + + + {% if person_post.elected %} + + {% else %} + + {% endif %} + + {% endfor %} +
+

Previous Elections

+
DateElectionPartyResults
{{ person_post.election.election_date|date:"Y" }}{{ person_post.post.label }}: {{ person_post.election }}{{ person_post.party.party_name }}{% if person_post.votes_cast %}{{ person_post.votes_cast|intcomma }} votes (elected){% else %}Vote count not available (elected){% endif %}{% if person_post.votes_cast %}{{ person_post.votes_cast|intcomma }} votes (not elected){% else %}Vote count not available (not elected){% endif %}
+
+
+{% endif %} diff --git a/wcivf/apps/people/templates/people/not_current_person_detail.html b/wcivf/apps/people/templates/people/not_current_person_detail.html index 134dfd036..1d98296c6 100644 --- a/wcivf/apps/people/templates/people/not_current_person_detail.html +++ b/wcivf/apps/people/templates/people/not_current_person_detail.html @@ -12,28 +12,33 @@ {% block page_meta %}{% endblock page_meta %} {% block content %} -
-

{{ object.name }} - {% if person.death_date and person.current_or_future_candidacies and person.personpost.party.is_independent %} - (Deceased) - {% endif %} -

+
+
+
+
+
+

+ {{ object.name }} + {% if person.display_deceased and person.personpost.party.is_independent %} + (Deceased) + {% endif %} +

+

{{ object.name }} stood for election {{ object.past_not_current_candidacies.count }} times.

+
+ {% if object.photo_url %} +
+ profile photo of {{ object.name }} +
+ {% endif %} +
+ + {% include "people/includes/_person_previous_elections_card.html" %} + + {% include "elections/includes/_postcode_search_form.html" %} + +
- -
-

Previous elections

-

This candidate has previously stood for election in:

-
    - {% for person_post in object.past_not_current_candidacies %} -
  • {{ person_post.election.election_date|date:"Y" }}: - {{ person_post.election }}, as the {{ person_post.party.party_name }} candidate for - {{ person_post.post.label }} - {{ person_post.post_election.short_cancelled_message_html }} - {% endfor %} -
-
- -{% include "elections/includes/_postcode_search_form.html" %} +
{% endblock content %} diff --git a/wcivf/apps/people/templates/people/person_detail.html b/wcivf/apps/people/templates/people/person_detail.html index 1e40e0362..858239465 100644 --- a/wcivf/apps/people/templates/people/person_detail.html +++ b/wcivf/apps/people/templates/people/person_detail.html @@ -10,356 +10,61 @@ {% block og_image %}{% if object.photo_url %}{{ object.photo_url }}{% endif %}{% endblock og_image %} {% block content %} -
-

{{ object.name }} - {% if person.death_date and person.current_or_future_candidacies and person.personpost.party.is_independent %} - (Deceased) - {% endif %} -

- {% if object.photo_url %} - profile photo of {{ object.name }} - {% endif %} +
+
+
+ {% include "people/includes/_person_intro_card.html" %} -

- {{ object.intro|safe }}. Our volunteers have been adding information on {{ object.name }} - here's everything we know so far! -

-
- - {% if object.has_any_contact_info %} -
-

Contact information

-
- {% if object.facebook_page_url or object.facebook_personal_url %} -
Facebook
-
- {% if object.facebook_personal_url %} - {% if object.facebook_page_url and object.facebook_personal_url %}Personal profile: {% endif %} - {{ object.facebook_personal_url }} -
- {% endif %} - {% if object.facebook_page_url %} - {% if object.facebook_page_url and object.facebook_personal_url %}Public page: {% endif %} - {{ object.facebook_page_url }} - - {% endif %} -
- {% endif %} - {% if object.linkedin_url %} -
LinkedIn
-
- - {{ object.linkedin_url }} - -
- {% endif %} - {% if object.homepage_url %} -
Home page
-
- - {{ object.homepage_url }} - -
- {% endif %} - {% if object.instagram_url %} -
Instagram
-
- - {{ object.instagram_url }} - -
- {% endif %} - {% if object.youtube_profile %} -
YouTube
-
- - {{ object.youtube_profile }} - -
- {% endif %} - {% if object.party_ppc_page_url %} -
The party's candidate page for this person
-
- - {{ object.party_ppc_page_url }} - -
- {% endif %} - {% if object.favourite_biscuit %} -
Favourite biscuit
-
- “{{ object.favourite_biscuit }}” -
- {% endif %} - {% if object.email and object.current_or_future_candidacies %} -
Email
-
{{ object.email }}
- {% else %} - We don't know {{ object.name }}'s email address. - Can you add it? - {% endif %} -
-
- {% endif %} - - {% if object.manifestos %} - {% with object.personpost.party as party %} -
- - {% if party.emblem %} - - - {% endif %} -

Party manifesto

-
-

- - {{ object.name }} {{ object.personpost.election.in_past|yesno:"was,is" }} the {{ party.party_name }} candidate. - Find out more about their policies in the {{ party.party_name }} manifesto. -

-
    - {% for manifesto in object.manifestos %} - {% include "parties/single_manifesto.html" %} - {% endfor %} -
-
-
- {% endwith %} - {% endif %} - - {% if object.statement_to_voters %} -
-

Statement to voters

-
{{ object.statement_to_voters|linebreaks }}
-

This statement was added by {{ object.name }}, their team, or by a Democracy Club volunteer, based on information published by the candidate elsewhere.

-
- {% endif %} - - {% if object.wikipedia_bio %} -
-

Wikipedia

-

{{ object.wikipedia_bio }}

- Read more on Wikipedia -
- {% endif %} + {% include "people/includes/_person_policy_card.html" %} -{# {% if object.leaflet_set.exists %}#} -{#
#} -{#

Recent leaflets from {{ object.name }}

#} -{# {% for leaflet in object.leaflet_set.latest_four %}#} -{# #} -{# {% endfor %}#} -{#

#} -{# More leaflets from {{ object.name }}#} -{# Upload a leaflet#} -{#

#} -{#
#} -{# {% endif %}#} + {% include "people/includes/_person_about_card.html" %} - {% if object.twfy_id %} -
-

Record in office

-

- See this candidate's record on TheyWorkForYou - their speeches, voting history and more. -

-
- {% endif %} - - -{#{% if object.associatedcompany_set.exists %}#} -{#
#} -{#

Associated companies

#} -{#

{{object.name}} is associated with the following companies:

#} -{#
    #} -{# {% for company in object.associatedcompany_set.all %}#} -{#
  • #} -{# {{ company.company_name }}#} -{#
    #} -{# {{ company.role }}{% if company.role_status %}({{ company.role_status }}){% endif %}#} -{# {{ company.role_appointed_date | date:"Y" }} – {{ company.role_resigned_date | date:"Y"}}#} -{#
    #} -{# #} -{# More details from Companies House#} -{# #} -{#
  • #} -{# {% endfor %}#} -{#
#} -{#
#} -{#{% endif %}#} - - {% if object.twitter_username %} - - {% endif %} + {% include "people/includes/_person_contact_card.html" %} -{#{% include "people/includes/facebook_adverts_card.html" %}#} + {% include "people/includes/_person_previous_elections_card.html" %} - {% if object.pledges.exists %} -
-

Candidate pledges

-

Hope Not Hate, a London-based charity, canvassed local residents and community organisations to establish the following questions to the candidates in this ward.

- {% for pledge in object.pledges.all %} -

{{ pledge.question }}

-
{{ pledge.answer|markdown }}
- {% endfor %} -
- {% endif %} - - - {% if object.local_party and object.current_or_future_candidacies %} -
-

{{ object.local_party.name }}

-

{{ object.name }}'s local party is the {{ object.local_party.name }}.

- {% if object.local_party.twitter %} -

- - @{{ object.local_party.twitter }} - on Twitter

- {% endif %} + {% include "people/includes/_person_edit_details_card.html" %} - {% if object.local_party.facebook_page %} -

- - {{ object.local_party.facebook_page }} - on Facebook

- {% endif %} + {% include "people/includes/_person_hustings_card.html" %} - {% if object.local_party.homepage %} -

- - {{ object.local_party.name }}'s website + {% if referer_postcode %} +

+

+ « Back to candidates in {{ referer_postcode }}

- {% endif %} - - {% if object.local_party.email %} -

- - Email {{ object.local_party.email }} -

- {% endif %} - -
- {% endif %} - -{% if object.postelection.husting_set.exists and object.current_or_future_candidacies %} -
-

Local hustings

-

You can meet candidates and question them at local hustings. Here are hustings where {{ object.name }} may be appearing.

- {% include "elections/includes/_hustings_list.html" with hustings=object.postelection.husting_set.all %} -
-{% endif %} - - -{% if object.past_not_current_candidacies %} -
-

Previous elections

-

This candidate has previously stood for election in:

-
    - {% for person_post in object.past_not_current_candidacies %} -
  • {{ person_post.election.election_date|date:"Y" }}: - {{ person_post.election }}, as the {{ person_post.party.party_name }} candidate for - {{ person_post.post.label }} - {{ person_post.post_election.short_cancelled_message_html }} - {% endfor %} -
-
-{% endif %} - - {% if referer_postcode %} - - {% endif %} - - {# Edit link #} - {% if object.current_or_future_candidacies %} -
-

That's all we know! Will you help us find more about this candidate?

- -

Our volunteers have been working hard to add information on as many candidates as possible, but they need help.

- - {% if object.should_show_email_cta %} -

Thousands of voters will rely on this site.{% else %}

{% endif %} - If you can add information that should be on this page - - {% if object.cta_example_details %} - - such as {{ object.name }}'s - {{ object.cta_example_details|join:", " }} - +

{% endif %} - please use our crowdsourcing website to add it.

- - Add or edit details » - - {% if object.should_show_email_cta %} -

You can also email {{ object.name }} directly to ask them to add information to this page. -

Ask the candidate for more information »

-{% endif %} -{#

Upload your leaflets

#} -{#

If you've received election leaflets from {{ object.name }}, please take a photo#} -{# of them and upload them to ElectionLeaflets.org

#} -{#

Add leaflets

#} + {% if not referer_postcode %} + {% include "elections/includes/_postcode_search_form.html" %} + {% endif %} +
- {% endif %} - - {% if not referer_postcode %} -{% include "elections/includes/_postcode_search_form.html" %} -{% endif %} - -{#{% include "feedback/feedback_form.html" %}#} +
{% endblock content %} - - - {% block breadcrumbs %} {% if referer_postcode %} - {# We've come from the postcode page #} - +{# We've come from the postcode page #} + {% elif object.personpost %} - {# There's a current post #} - +{# There's a current post #} + {% endif %} {% include "elections/includes/_ld_candidate.html" with person=object.personpost.person party=object.personpost.party %} -{% endblock breadcrumbs %} +{% endblock breadcrumbs %} \ No newline at end of file diff --git a/wcivf/apps/people/tests/test_person_models.py b/wcivf/apps/people/tests/test_person_models.py new file mode 100644 index 000000000..ae70e4889 --- /dev/null +++ b/wcivf/apps/people/tests/test_person_models.py @@ -0,0 +1,71 @@ +from elections.tests.factories import ( + ElectionFactory, +) +from parties.tests.factories import PartyFactory +from people.tests.factories import PersonFactory, PersonPostFactory +from django.test import TestCase + + +class TestPersonModel(TestCase): + def setUp(self): + self.person = PersonFactory() + + def test_long_statement(self): + self.person.statement_to_voters = "Lorem ipsum dolor sit amet, yo consectetur adipiscing elit. Nam eget metus dui. Integer a velit viverra, interdum dui eget, lobortis massa. Nam pharetra, risus eu gravida ullamcorper, enim nisl ornare lectus, at eleifend tellus lacus quis velit. Sed ornare volutpat aliquam. Donec fermentum dapibus odio, vel gravida augue ornare non. Donec ultrices consequat ullamcorper. Proin ac ante et tellus ultrices aliquam lobortis quis orci. Nunc eget eros facilisis, sagittis nibh quis, feugiat purus. Integer nibh erat, dapibus eget vehicula non, tincidunt at eros. Etiam varius ultricies mollis. Morbi eros diam, hendrerit eu condimentum maximus, lobortis sit amet turpis. Cras quam." + assert self.person.long_statement is True + + def test_short_statement(self): + self.person.statement_to_voters = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing eight" + ) + assert self.person.long_statement is False + + def test_statement_count(self): + self.person.statement_to_voters = "Lorem ipsum dolor sit amet" + assert self.person.statement_count == 5 + + def test_statement_intro(self): + self.person.statement_to_voters = "Lorem ipsum dolor sit amet.\nConsectetur adipiscing elit.\nVivamus est eleven." + assert self.person.statement_intro == "Lorem ipsum dolor sit amet." + + def test_statement_remainder(self): + self.person.statement_to_voters = "Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Vivamus est eleven." + assert ( + self.person.statement_remainder + == " Consectetur adipiscing elit. Vivamus est eleven." + ) + + def test_display_deceased(self): + self.party = PartyFactory() + self.election = ElectionFactory() + self.personpost = PersonPostFactory( + person=self.person, party=self.party, election=self.election + ) + self.person.death_date = "01/01/2021" + assert self.person.display_deceased is True + + def test_facebook_personal_username(self): + self.person.facebook_personal_url = ( + "https://www.facebook.com/vicky.ford.142" + ) + assert self.person.facebook_personal_username == "vicky.ford.142" + + def test_facebook_username(self): + self.person.facebook_page_url = ( + "https://www.facebook.com/vicky4chelmsford" + ) + assert self.person.facebook_username == "vicky4chelmsford" + + def test_instagram_username(self): + self.person.instagram_url = "https://www.instagram.com/vickyfordmp" + assert self.person.instagram_username == "vickyfordmp" + + def test_linkedin_username(self): + self.person.linkedin_url = "https://www.linkedin.com/in/vicky-ford/" + assert self.person.linkedin_username == "vicky-ford" + + def test_youtube_username(self): + self.person.youtube_profile = ( + "https://www.youtube.com/user/pierscorbyn2" + ) + assert self.person.youtube_username == "pierscorbyn2" diff --git a/wcivf/apps/people/tests/test_person_views.py b/wcivf/apps/people/tests/test_person_views.py index 92658e729..25e8def92 100644 --- a/wcivf/apps/people/tests/test_person_views.py +++ b/wcivf/apps/people/tests/test_person_views.py @@ -1,16 +1,16 @@ -import pytest +from unittest.mock import MagicMock +import pytest from django.test import TestCase from django.test.utils import override_settings -from unittest.mock import MagicMock - -from people.tests.factories import PersonFactory, PersonPostFactory -from parties.tests.factories import PartyFactory from elections.tests.factories import ( ElectionFactory, - PostFactory, + ElectionFactoryLazySlug, PostElectionFactory, + PostFactory, ) +from parties.tests.factories import PartyFactory +from people.tests.factories import PersonFactory, PersonPostFactory from people.views import PersonView @@ -41,8 +41,8 @@ def test_not_current_person_view(self): self.assertTemplateUsed( response, "people/not_current_person_detail.html" ) - self.assertContains(response, "Previous elections") - self.assertNotContains(response, "Contact information") + self.assertContains(response, f"{ self.person.name} stood for election") + self.assertNotContains(response, f"{ self.person.name} Online") self.assertContains(response, '') def test_not_current_person_with_twfy_id(self): @@ -84,7 +84,7 @@ def test_correct_elections_listed(self): response = self.client.get(self.person_url, follow=True) self.assertContains(response, election_name) - self.assertContains(response, "is the") + self.assertContains(response, "is a") def test_election_in_past_listed(self): response = self.client.get(self.person_url, follow=True) @@ -113,7 +113,205 @@ def test_election_in_past_listed(self): response = self.client.get(self.person_url, follow=True) self.assertContains(response, election_name) - self.assertContains(response, "was the") + self.assertContains(response, "was a") + + def test_multiple_candidacies_intro(self): + election_one = ElectionFactory() + election_two = ElectionFactoryLazySlug() + party = PartyFactory(party_name="Liberal Democrat", party_id="foo") + PersonPostFactory( + person=self.person, election=election_one, party=party + ) + PersonPostFactory( + person=self.person, election=election_two, party=party + ) + response = self.client.get(self.person_url, follow=True) + self.assertContains( + response, + "is a Liberal Democrat candidate in the following elections:", + ) + + def test_multiple_independent_candidacies_intro(self): + election_one = ElectionFactory() + election_two = ElectionFactoryLazySlug() + party = PartyFactory(party_name="Independent", party_id="ynmp-party:2") + PersonPostFactory( + person=self.person, election=election_one, party=party + ) + PersonPostFactory( + person=self.person, election=election_two, party=party + ) + response = self.client.get(self.person_url, follow=True) + self.assertContains( + response, "is an Independent candidate in the following elections:" + ) + + def test_one_candidacy_intro(self): + election = ElectionFactory() + party = PartyFactory( + party_name="Conservative and Unionist Party", party_id="ConUnion" + ) + person_post = PersonPostFactory( + person=self.person, election=election, party=party + ) + response = self.client.get(self.person_url, follow=True) + self.assertContains( + response, + f"{self.person.name} is a {party.party_name} candidate in {person_post.post.label} in the {election.name}.", + ) + + def test_no_previous_elections(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Previous Elections") + + def test_previous_elections(self): + past_election = ElectionFactoryLazySlug( + election_date="2019-05-02", current=False + ) + party = PartyFactory(party_name="Liberal Democrat", party_id="foo") + PersonPostFactory( + person=self.person, + post_election__election=past_election, + election=past_election, + party=party, + votes_cast=1000, + ) + response = self.client.get(self.person_url, follow=True) + self.assertContains(response, "Previous Elections") + + def test_no_statement_to_voters(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Statement to voters") + + def test_statement_to_voters(self): + self.person.statement_to_voters = "I believe in equal rights." + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "Statement to voters") + + def test_no_TWFY(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Record in office") + + def test_TWFY(self): + self.person.twfy_id = 123 + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "Record in office") + + def test_no_wikipedia(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Wikipedia") + + def test_wikipedia(self): + self.person.wikipedia_bio = "yo" + self.person.wikipedia_url = "https//www.wikipedia.com/yo" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "Wikipedia") + + def test_no_facebook(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "username") + + def test_facebook(self): + self.person.facebook_personal_url = "https//www.facebook.com/yo" + self.person.facebook_page_url = "https//www.facebook.com/yo" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "yo") + + def test_no_linkedin(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "LinkedIn") + + def test_linkedin(self): + self.person.linkedin_url = "https://www.linkedin.com/yo" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "LinkedIn") + + def test_instagram(self): + self.person.instagram_url = "https://www.instagram.com/yo" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "Instagram") + + def test_no_instagram(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Instagram") + + def test_party_page(self): + self.person.party_ppc_page_url = "https://www.voteforme.com/bob" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains( + response, "The party's candidate page for this person" + ) + + def test_no_party_page(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains( + response, "The party's candidate page for this person" + ) + + def test_no_youtube(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "YouTube") + + def test_youtube(self): + self.person.youtube_profile = "Mary123" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "YouTube") + + def test_email(self): + self.person.email = "me@voteforme.com" + self.person.save() + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertContains(response, "Email") + + def test_no_email(self): + PersonPostFactory(person=self.person, election=ElectionFactory()) + response = self.client.get(self.person_url, follow=True) + self.assertEqual(response.template_name, ["people/person_detail.html"]) + self.assertNotContains(response, "Email") class TestPersonViewUnitTests: diff --git a/wcivf/apps/people/views.py b/wcivf/apps/people/views.py index ee5acbec1..8856600de 100644 --- a/wcivf/apps/people/views.py +++ b/wcivf/apps/people/views.py @@ -135,7 +135,7 @@ def get_intro(self, person): intro.append("is") else: intro.append("was") - + # clear CTAs below if person.personpost: party = person.personpost.party if party: @@ -144,9 +144,8 @@ def get_intro(self, person): elif party.party_name == "Speaker seeking re-election": intro.append("the Speaker seeking re-election") else: - intro.append("the") - str = '' - str += party.party_name + " candidate" + intro.append("a") + str = party.party_name + " candidate" intro.append(str) else: intro.append("a candidate") @@ -157,13 +156,11 @@ def get_intro(self, person): ): intro.append("the constituency of") if person.postelection: - str = '' + person.personpost.post.label + "" + str = person.personpost.post.label else: str = person.personpost.post.label - str += ' in the ' + person.personpost.election.name + "" + str += " in the " + str += person.personpost.election.name intro.append(str) if person.personpost.votes_cast: diff --git a/wcivf/assets/scss/main.scss b/wcivf/assets/scss/main.scss index fe6fc3aca..638e6b128 100644 --- a/wcivf/assets/scss/main.scss +++ b/wcivf/assets/scss/main.scss @@ -3,6 +3,75 @@ @import 'feedback_form'; @import '_fontello'; +@import 'partials/_fonts.scss'; +@import 'partials/_variables.scss'; +@import 'partials/_type.scss'; +@import 'partials/_forms.scss'; +@import 'partials/_description-lists.scss'; +@import 'partials/_button.scss'; +@import 'partials/_call-to-action.scss'; +@import 'partials/_details.scss'; +@import 'partials/_stack.scss'; +@import 'partials/_cluster.scss'; +@import 'partials/_grid.scss'; +@import 'partials/_sidebar.scss'; +@import 'partials/_breadcrumbs.scss'; +@import 'partials/_card.scss'; +@import 'partials/_candidate.scss'; +@import 'partials/_table.scss'; +@import 'partials/_header.scss'; +@import 'partials/_subnav.scss'; +@import 'partials/_footer.scss'; +@import 'partials/_radio.scss'; +@import 'partials/_checkbox.scss'; +@import 'partials/_select.scss'; +@import 'partials/_filter.scss'; +@import 'partials/_dark.scss'; +@import 'partials/_page.scss'; +@import 'partials/_utilities.scss'; + +// // Generate atomic classes for documentation demos +@import 'partials/_classes-colors.scss'; +@import 'partials/_classes-scales.scss'; + +$scope: true; + +@mixin optional-styles { + @include description-lists; + @include button; + @include cta; + @include card; + @include candidate; + @include cluster; + @include grid; + @include stack; + @include breadcrumbs; + @include table; + @include header; + @include radio; + @include checkbox; + @include select; +} + +@if $scope { + .ds-scope { + @include type; + @include forms; + @include optional-styles; + @include utilities; + } +} @else { + @include type; + @include forms; + @include optional-styles; + @include utilities; +} + +main { + background-color: white; +} + + footer { ul { overflow: hidden; @@ -92,4 +161,4 @@ dd { summary { cursor: pointer; color: $dc-bright-pink; -} +} \ No newline at end of file diff --git a/wcivf/settings/base.py b/wcivf/settings/base.py index e65aa1391..6e35860df 100644 --- a/wcivf/settings/base.py +++ b/wcivf/settings/base.py @@ -151,6 +151,12 @@ extra_js=["js/scripts.js", "feedback/js/feedback_form.js"], ) +import dc_design_system + +PIPELINE["SASS_ARGUMENTS"] += ( + " -I " + dc_design_system.DC_SYSTEM_PATH + "/system" +) + CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache",