diff --git a/opentech/apply/activity/forms.py b/opentech/apply/activity/forms.py index 1931ffde3a71848d774ef2b3251679a640e5eea1..180975a037a77fd350ebe15b191eed6e54aea23d 100644 --- a/opentech/apply/activity/forms.py +++ b/opentech/apply/activity/forms.py @@ -6,4 +6,10 @@ from .models import Activity class CommentForm(forms.ModelForm): class Meta: model = Activity - fields = ('message',) + fields = ('message', 'visibility') + labels = { + 'visibility': '', + } + widgets = { + 'visibility': forms.RadioSelect(), + } diff --git a/opentech/apply/activity/migrations/0003_activity_visibility.py b/opentech/apply/activity/migrations/0003_activity_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..d768009b9b1c99f67821fb0b893b6c008253dc66 --- /dev/null +++ b/opentech/apply/activity/migrations/0003_activity_visibility.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.8 on 2018-03-06 11:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0002_activity_type'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='visibility', + field=models.CharField(choices=[('public', 'Public'), ('internal', 'Internal')], default='public', max_length=10), + ), + ] diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py index ae0acc1145982ecaa2a44d76c5c9a88153b17a2b..9ae8a7d595f753847f4f79df6c8dd7ad0cb7cb50 100644 --- a/opentech/apply/activity/models.py +++ b/opentech/apply/activity/models.py @@ -9,6 +9,14 @@ ACTIVITY_TYPES = { ACTION: 'Action', } +PUBLIC = 'public' +INTERNAL = 'internal' + +VISIBILITY = { + PUBLIC: 'Public', + INTERNAL: 'Internal', +} + class ActivityBaseManager(models.Manager): def create(self, **kwargs): @@ -33,6 +41,7 @@ class Activity(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL) submission = models.ForeignKey('funds.ApplicationSubmission', related_name='activities') message = models.TextField() + visibility = models.CharField(choices=VISIBILITY.items(), default=PUBLIC, max_length=10) objects = models.Manager() comments = CommentManger() @@ -41,5 +50,9 @@ class Activity(models.Model): class Meta: ordering = ['-timestamp'] + @property + def private(self): + return self.visibility != PUBLIC + def __str__(self): return '{}: for "{}"'.format(self.get_type_display(), self.submission) diff --git a/opentech/apply/activity/templates/activity/include/action_list.html b/opentech/apply/activity/templates/activity/include/action_list.html index ebd8bbaa11232a7312f180a2ec98681b9ec50958..5451267538448089e4cbfb6bf4aa79f52956f75e 100644 --- a/opentech/apply/activity/templates/activity/include/action_list.html +++ b/opentech/apply/activity/templates/activity/include/action_list.html @@ -1,3 +1,5 @@ {% for action in actions %} {% include "activity/include/listing_base.html" with activity=action %} +{% empty %} +There are no actions. {% endfor %} diff --git a/opentech/apply/activity/templates/activity/include/comment_form.html b/opentech/apply/activity/templates/activity/include/comment_form.html index 57287e67938d2f3965d654f140eab7084d56f47f..39cd0da890468dd32813e2ff65718601d51057cc 100644 --- a/opentech/apply/activity/templates/activity/include/comment_form.html +++ b/opentech/apply/activity/templates/activity/include/comment_form.html @@ -1,6 +1,8 @@ <h4>Add communication</h4> -<form class="form form--comments" method="post" id="comment-form"> - {% csrf_token %} - {{ comment_form }} - <button id="comment-form-submit" name="form-submitted" form="comment-form" class="button button--primary" type="submit" value="comment">Add message</button> -</form> +<div class="wrapper wrapper--comments"> + <form class="form form--comments" method="post" id="comment-form"> + {% csrf_token %} + {{ comment_form }} + <button id="comment-form-submit" name="form-submitted" form="comment-form" class="button button--primary" type="submit" value="comment">Add message</button> + </form> +</div> diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html index 8d44760d741a3ec26f32fc3f51b9dc0ba634d1dc..43086846bfe4c280ce78ec60741f9370c2520f5f 100644 --- a/opentech/apply/activity/templates/activity/include/listing_base.html +++ b/opentech/apply/activity/templates/activity/include/listing_base.html @@ -1,5 +1,21 @@ -<div> - <p>{{ activity.timestamp }}</p> - <p>{{ activity.user }}</p> - <p>{{ activity.message }}</p> +<div class="feed__item feed__item--{{ activity.type }}"> + {% if activity.private %} + <svg class="icon icon--private-eye"><use xlink:href="#private-eye"></use></svg> + {% endif %} + <div class="feed__pre-content"> + <p class="feed__label feed__label--{{ activity.type }}">{{ activity.type|capfirst }}</p> + </div> + <div class="feed__content"> + <div class="feed__meta"> + <p class="feed__label feed__label--{{ activity.type }} feed__label--mobile">{{ activity.type|capfirst }}</p> + <p class="feed__meta-item">{{ activity.timestamp|date:"m.d.y h:iA e" }}</p> + </div> + <div class="feed__heading"> + <h4 class="feed__name">{{ activity.user }}</h4> + {% if submission_title %} + <h6>updated <a href="{{ activity.submission.get_absolute_url }}">{{ activity.submission.title }}</a></h6> + {% endif %} + </div> + <p class="feed__teaser">{{ activity.message }}</p> + </div> </div> diff --git a/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..a4e75b2c5373609943cb68a1691255d8f38eae66 --- /dev/null +++ b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html @@ -0,0 +1,42 @@ +{% extends "base-apply.html" %} +{% load render_table from django_tables2 %} +{% load wagtailcore_tags %} + +{% block title %}Submission Dashboard{% endblock %} + +{% block content %} +<div class="wrapper wrapper--breakout wrapper--admin"> + <div class="wrapper wrapper--large wrapper--applicant-dashboard"> + <div> + <h3 class="heading heading--no-margin">Dashboard</h3> + <h5>An overview of active and past submissions</h5> + </div> + <div class="wrapper wrapper--apply-box"> + <h4 class="heading heading--no-margin">Submit a new application</h4> + <h5 class="heading heading--normal">Apply now for our open rounds</h5> + <a class="button button--primary" href="{% pageurl APPLY_SITE.root_page %}" class="button">Apply</a> + </div> + </div> +</div> +<div class="wrapper wrapper--large wrapper--inner-space-medium"> + <h3>Your active submissions</h3> + {% for submission in my_active_submissions %} + <div class="wrapper wrapper--status-bar"> + <div> + <h5 class="heading heading--no-margin"><a class="link link--underlined" href="{% url 'funds:submission' submission.id %}">{{ submission.title }}</a></h5> + <h6 class="heading heading--no-margin heading--submission-meta"><span>Submitted:</span> {{ submission.submit_time.date }} by {{ submission.user.get_full_name }}</h6> + </div> + {% include "funds/includes/status_bar.html" with workflow=submission.workflow status=submission.status class="status-bar--small" %} + </div> + {% empty %} + No active submissions + {% endfor %} +</div> + +{% if table.data %} + <div class="wrapper wrapper--large wrapper--inner-space-medium"> + <h3>Submission history</h3> + {% render_table table %} + </div> +{% endif %} +{% endblock %} diff --git a/opentech/apply/dashboard/views.py b/opentech/apply/dashboard/views.py index 83d4bb09918d3ff92e04af45f6b102bf251a8525..1801a6d8355d2f672f3023dc3039629c965a0be8 100644 --- a/opentech/apply/dashboard/views.py +++ b/opentech/apply/dashboard/views.py @@ -1,5 +1,41 @@ -from django.views.generic import TemplateView +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator +from django.views.generic import TemplateView, View +from django_tables2.views import SingleTableView -class DashboardView(TemplateView): +from opentech.apply.funds.models import ApplicationSubmission +from opentech.apply.funds.tables import SubmissionsTable +from opentech.apply.users.groups import STAFF_GROUP_NAME + + +class AdminDashboardView(TemplateView): template_name = 'dashboard/dashboard.html' + + +class ApplicantDashboardView(SingleTableView): + template_name = 'dashboard/applicant_dashboard.html' + model = ApplicationSubmission + table_class = SubmissionsTable + + def get_queryset(self): + return self.model.objects.filter(user=self.request.user).inactive() + + def get_context_data(self, **kwargs): + my_active_submissions = self.model.objects.filter(user=self.request.user).active() + + return super().get_context_data( + my_active_submissions=my_active_submissions, + **kwargs, + ) + + +@method_decorator(login_required, name='dispatch') +class DashboardView(View): + def dispatch(self, request): + if request.user.groups.filter(name=STAFF_GROUP_NAME).exists(): + view = AdminDashboardView + else: + view = ApplicantDashboardView + + return view.as_view()(request) diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py index c36510831779e8db5128b3da927b7a8b58fbf82e..5cc50910a7f9a84c67ff8ca9367408cbb721404e 100644 --- a/opentech/apply/funds/models.py +++ b/opentech/apply/funds/models.py @@ -36,10 +36,10 @@ from opentech.apply.stream_forms.blocks import UploadableMediaBlock from opentech.apply.stream_forms.models import AbstractStreamForm from opentech.apply.users.groups import STAFF_GROUP_NAME +from .admin_forms import WorkflowFormAdminForm from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel -from .admin_forms import WorkflowFormAdminForm -from .workflow import SingleStage, DoubleStage +from .workflow import SingleStage, DoubleStage, active_statuses WORKFLOW_CLASS = { @@ -417,7 +417,14 @@ class LabForm(AbstractRelatedForm): class JSONOrderable(models.QuerySet): + json_field = '' + def order_by(self, *field_names): + if not self.json_field: + raise ValueError( + 'json_field cannot be blank, please provide a field on which to perform the ordering' + ) + def build_json_order_by(field): if field.replace('-', '') not in REQUIRED_BLOCK_NAMES: return field @@ -427,12 +434,22 @@ class JSONOrderable(models.QuerySet): field = field[1:] else: descending = False - return OrderBy(RawSQL("LOWER(form_data->>%s)", (field,)), descending=descending) + return OrderBy(RawSQL(f'LOWER({self.json_field}->>%s)', (field,)), descending=descending) field_ordering = [build_json_order_by(field) for field in field_names] return super().order_by(*field_ordering) +class ApplicationSubmissionQueryset(JSONOrderable): + json_field = 'form_data' + + def active(self): + return self.filter(status__in=active_statuses) + + def inactive(self): + return self.exclude(status__in=active_statuses) + + class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission): field_template = 'funds/includes/submission_field.html' @@ -447,7 +464,7 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission): # Workflow inherited from WorkflowHelpers status = models.CharField(max_length=254) - objects = JSONOrderable.as_manager() + objects = ApplicationSubmissionQueryset.as_manager() @property def status_name(self): @@ -461,6 +478,10 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission): def phase(self): return self.workflow.current(self.status) + @property + def active(self): + return self.phase.active + def ensure_user_has_account(self): if self.user and self.user.is_authenticated(): self.form_data['email'] = self.user.email diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py index abe2f37c08f4407421c2c67001da1c8f8132b903..732ae2754bcf70890cd17f3ae10f26721b0c7a57 100644 --- a/opentech/apply/funds/tables.py +++ b/opentech/apply/funds/tables.py @@ -14,26 +14,37 @@ from .widgets import Select2MultiCheckboxesWidget class SubmissionsTable(tables.Table): + """Base table for listing submissions, do not include admin data to this table""" title = tables.LinkColumn('funds:submission', args=[A('pk')], orderable=True) submit_time = tables.DateColumn(verbose_name="Submitted") status_name = tables.Column(verbose_name="Status") stage = tables.Column(verbose_name="Type") page = tables.Column(verbose_name="Fund") - lead = tables.Column(accessor='round.specific.lead', verbose_name='Lead') class Meta: model = ApplicationSubmission fields = ('title', 'status_name', 'stage', 'page', 'round', 'submit_time') - sequence = ('title', 'status_name', 'stage', 'page', 'round', 'lead', 'submit_time') + sequence = ('title', 'status_name', 'stage', 'page', 'round', 'submit_time') template = 'funds/tables/table.html' + row_attrs = { + 'class': lambda record: '' if record.active else 'is-inactive' + } def render_user(self, value): return value.get_full_name() - def render_status_name(self, value): + def render_status_name(self, value, record): return mark_safe(f'<span>{ value }</span>') +class AdminSubmissionsTable(SubmissionsTable): + """Adds admin only columns to the submissions table""" + lead = tables.Column(accessor='round.specific.lead', verbose_name='Lead') + + class Meta: + sequence = ('title', 'status_name', 'stage', 'page', 'round', 'lead', 'submit_time') + + def get_used_rounds(request): return Round.objects.filter(submissions__isnull=False).distinct() diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html index 12bfdb67c6b6caee84911a3c6a92a3928fda6427..9a3f581a0e153da46a5697c800f815319d6a7b43 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html @@ -12,62 +12,97 @@ <span>Lead: {{ object.lead }}</span> </h5> {% include "funds/includes/status_bar.html" with workflow=object.workflow status=object.phase %} + + <div class="tabs js-tabs"> + <div class="tabs__container"> + <a class="tab__item tab__item--active" href="#submission-details" data-tab="tab-1"> + Submission Details + </a> + + <a class="tab__item" href="#communications" data-tab="tab-2"> + Communications + </a> + + <a class="tab__item" href="#activity-feed" data-tab="tab-3"> + Activity Feed + </a> + </div> + </div> </div> </div> -<div class="wrapper wrapper--medium wrapper--inner-space-medium"> - <div class="wrapper wrapper--sidebar"> - {% include "funds/includes/actions.html" with mobile="true" %} - <section class="section section--has-sidebar"> - <h6 class="heading heading--submission-meta"> - <span>Submitted: </span>{{ object.submit_time.date }} by {{ object.user.get_full_name }} - </h6> +<div class="wrapper wrapper--medium wrapper--tabs"> +{# Tab 1 #} + <div class="tabs__content tabs__content--current" id="tab-1"> + {% include "funds/includes/actions.html" with mobile=True %} + <div class="wrapper wrapper--sidebar"> + <div> + <h6 class="heading heading--submission-meta"> + <span>Submitted: </span>{{ object.submit_time.date }} by {{ object.user.get_full_name }} + </h6> - <h3>Proposal Information</h3> - <div class="grid grid--proposal-info"> - <div> - <h5>Requested Funding</h5> - <p>{{ object.value }}</p> - </div> + <h3>Proposal Information</h3> + <div class="grid grid--proposal-info"> + <div> + <h5>Requested Funding</h5> + <p>{{ object.value }}</p> + </div> - <div> - <h5>Project Duration</h5> - <p>{{ object.value }}</p> - </div> + <div> + <h5>Project Duration</h5> + <p>{{ object.value }}</p> + </div> - <div> - <h5>Legal Name</h5> - <p>{{ object.full_name }}</p> - </div> + <div> + <h5>Legal Name</h5> + <p>{{ object.full_name }}</p> + </div> - <div> - <h5>Email</h5> - <p>{{ object.email }}</p> + <div> + <h5>Email</h5> + <p>{{ object.email }}</p> + </div> + </div> + <div class="rich-text rich-text--answers"> + {{ object.render_answers }} </div> </div> <div class="rich-text rich-text--answers"> {{ object.render_answers }} </div> - </section> + </div> <aside class="sidebar"> + {% include "funds/includes/actions.html" %} {% include "funds/includes/progress_form.html" %} {% include "funds/includes/update_lead_form.html" %} - {% include "funds/includes/actions.html" %} {% if other_submissions %} - <div class="sidebar__inner"> - <h6 class="heading heading--light-grey heading--small heading--uppercase">Past Submissions</h6> + <div class="sidebar__inner"> + <h6 class="heading heading--light-grey heading--small heading--uppercase">Past Submissions</h6> - {% for submission in other_submissions %} - <h6><a class="link link--underlined link--bold" href="{% url 'funds:submission' submission.id %}">{{ submission.title }}</a></h6> - {% endfor %} - </div> - {% endif %} + {% for submission in other_submissions %} + <h6><a class="link link--underlined link--bold" href="{% url 'funds:submission' submission.id %}">{{ submission.title }}</a></h6> + {% endfor %} + </div> + {% endif %} + </aside> + </div> + </div> + + {# Tab 2 #} + <div class="tabs__content" id="tab-2"> + <div class="feed"> {% include "activity/include/comment_form.html" %} {% include "activity/include/comment_list.html" %} + </div> + </div> + + {# Tab 3 #} + <div class="tabs__content" id="tab-3"> + <div class="feed"> {% include "activity/include/action_list.html" %} - </aside> + </div> </div> </div> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/includes/activity-feed.html b/opentech/apply/funds/templates/funds/includes/activity-feed.html new file mode 100644 index 0000000000000000000000000000000000000000..e51c07d70dd324dea8ce5539f33b116a9ff08e9a --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/activity-feed.html @@ -0,0 +1,34 @@ +<div class="js-activity-feed activity-feed"> + <div class="activity-feed__header"> + <div class="wrapper wrapper--medium wrapper--relative"> + <h4 class="activity-feed__heading">Activity Feed <svg class="icon icon--speech-bubble"><use xlink:href="#speech-bubble"></use></svg></h4> + <a href="#" class="js-close-feed link link--close-feed">Close</a> + <div class="tabs js-tabs"> + <div class="tabs__container"> + <a class="tab__item tab__item--active" href="#" data-tab="tab-1">Communications</a> + <a class="tab__item" href="#" data-tab="tab-2">Activity</a> + <a class="tab__item" href="#" data-tab="tab-3">All</a> + </div> + </div> + </div> + </div> + + <div class="wrapper wrapper--medium wrapper--activity-feed"> + <div class="tabs__content tabs__content--current" id="tab-1"> + {% include "activity/include/comment_list.html" with submission_title=True %} + </div> + + <div class="tabs__content" id="tab-2"> + {% include "activity/include/action_list.html" with submission_title=True %} + </div> + + <div class="tabs__content" id="tab-3"> + {% include "activity/include/all_activity_list.html" with submission_title=True %} + </div> + </div> + + <a href="#" class="js-to-top link link--to-top"> + <svg class="icon icon--to-top"><use xlink:href="#chevron"></use></svg> + <h6>Back to top</h6> + </a> +</div> diff --git a/opentech/apply/funds/templates/funds/includes/status_bar.html b/opentech/apply/funds/templates/funds/includes/status_bar.html index e537515025ce4cea59a39f9c96c8e192d1455c3d..79fd9c7ad9b167128a0cc246067b58cc4f098c8c 100644 --- a/opentech/apply/funds/templates/funds/includes/status_bar.html +++ b/opentech/apply/funds/templates/funds/includes/status_bar.html @@ -1,4 +1,4 @@ -<div class="status-bar"> +<div class="status-bar {{ class }}"> {% for phase in status.stage %} <div class="status-bar__item {% if phase == status %} diff --git a/opentech/apply/funds/templates/funds/submissions.html b/opentech/apply/funds/templates/funds/submissions.html index f039d269e2cac92256acb8b8ece3c1a753a740aa..fc422f16227f30ef0d2fb4b5e719cedcb87621c6 100644 --- a/opentech/apply/funds/templates/funds/submissions.html +++ b/opentech/apply/funds/templates/funds/submissions.html @@ -39,9 +39,11 @@ {% render_table table %} </div> -{% include "activity/include/comment_list.html" %} -{% include "activity/include/action_list.html" %} -{% include "activity/include/all_activity_list.html" %} +<a href="#" class="js-open-feed link link--open-feed"> + <h4 class="heading heading--no-margin heading--activity-feed">Activity Feed</h4> +</a> + +{% include "funds/includes/activity-feed.html" %} {% endblock %} diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index cc28bdbff046fb84a8c23c3e8e1a9cc7a9e83d17..1ca6807eafbe79d93d5dd964f1b8faa2cbe8851a 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -15,13 +15,13 @@ from opentech.apply.activity.models import Activity from .forms import ProgressSubmissionForm, UpdateSubmissionLeadForm from .models import ApplicationSubmission -from .tables import SubmissionsTable, SubmissionFilter, SubmissionFilterAndSearch +from .tables import AdminSubmissionsTable, SubmissionFilter, SubmissionFilterAndSearch from .workflow import SingleStage, DoubleStage class SubmissionListView(AllActivityContextMixin, SingleTableMixin, FilterView): template_name = 'funds/submissions.html' - table_class = SubmissionsTable + table_class = AdminSubmissionsTable filterset_class = SubmissionFilter @@ -32,7 +32,7 @@ class SubmissionListView(AllActivityContextMixin, SingleTableMixin, FilterView): class SubmissionSearchView(SingleTableMixin, FilterView): template_name = 'funds/submissions_search.html' - table_class = SubmissionsTable + table_class = AdminSubmissionsTable filterset_class = SubmissionFilterAndSearch diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py index 1f621ec609ed66a582b416f3ec84f94fcc3b7e8d..02fd0f82191ef82d0d71047b83e0633c53d2a057 100644 --- a/opentech/apply/funds/workflow.py +++ b/opentech/apply/funds/workflow.py @@ -1,7 +1,8 @@ from collections import defaultdict import copy +import itertools -from typing import Dict, Iterable, Iterator, List, Sequence, Type, Union +from typing import Dict, Iterable, Iterator, List, Sequence, Set, Type, Union from django.forms import Form from django.utils.text import slugify @@ -226,10 +227,12 @@ class Phase: name: str = '' public_name: str = '' - def __init__(self, name: str='', public_name: str ='') -> None: + def __init__(self, name: str='', public_name: str ='', active: bool=True) -> None: if name: self.name = name + self.active = active + if public_name: self.public_name = public_name elif not self.public_name: @@ -329,9 +332,9 @@ class DiscussionWithNextPhase(Phase): actions = [NextPhaseAction('Open Review'), reject_action] -rejected = Phase(name='Rejected') +rejected = Phase(name='Rejected', active=False) -accepted = Phase(name='Accepted') +accepted = Phase(name='Accepted', active=False) class RequestStage(Stage): @@ -377,3 +380,24 @@ class DoubleStage(Workflow): statuses = set(phase.name for phase in Phase.__subclasses__()) status_options = [(slugify(opt), opt) for opt in statuses] + + +def get_active_statuses() -> Set[str]: + active = set() + + def add_if_active(phase: 'Phase') -> None: + if phase.active: + active.add(str(phase)) + + for phase in itertools.chain(SingleStage([None]), DoubleStage([None, None])): + try: + add_if_active(phase) + except AttributeError: + # it is actually a step + step = phase + for phase in step.phases: + add_if_active(phase) + return active + + +active_statuses = get_active_statuses() diff --git a/opentech/apply/users/management/__init__.py b/opentech/apply/users/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/users/management/commands/__init__.py b/opentech/apply/users/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/users/management/commands/migrate_users.py b/opentech/apply/users/management/commands/migrate_users.py new file mode 100644 index 0000000000000000000000000000000000000000..7d8ef0b505ce603d844db65441b76ba9263d3a83 --- /dev/null +++ b/opentech/apply/users/management/commands/migrate_users.py @@ -0,0 +1,91 @@ +import argparse +import json + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.core.management.base import BaseCommand +from django.db import transaction + +from opentech.apply.users.groups import STAFF_GROUP_NAME + + +class Command(BaseCommand): + help = "User migration script. Requires a source JSON file." + groups = Group.objects.all() + + def add_arguments(self, parser): + parser.add_argument('source', type=argparse.FileType('r'), help="Migration source JSON file") + parser.add_argument('--anonymize', action='store_true', help="Anonymizes non-OTF emails") + + @transaction.atomic + def handle(self, *args, **options): + with options['source'] as json_data: + User = get_user_model() + users = json.load(json_data) + + for uid in users: + user = users[uid] + + full_name = self.get_full_name(user) + user_object, created = User.objects.get_or_create( + email=self.get_email(user, options['anonymize']), + defaults={ + 'full_name': full_name, + 'drupal_id': uid, + } + ) + + operation = "Imported" if created else "Processed" + + groups = self.get_user_groups(user) + user_object.groups.set(groups) + + # Ensure uid is set + user_object.drupal_id = uid + user_object.save() + + self.stdout.write(f"{operation} user {uid} ({full_name})") + + def get_full_name(self, user): + full_name = user.get('field_otf_real_name', None) + try: + # The Drupal data structure includes a language reference. + # The default is 'und' (undefined). + full_name = full_name['und'][0]['safe_value'] + except (KeyError, TypeError): + full_name = user['name'] + + return full_name + + def get_user_groups(self, user): + groups = [] + role_map = { + 'proposer': 'Applicant', + 'council': 'Advisor', + 'administrator': 'Administrator', + 'dev': 'Administrator', + } + + if self.is_staff(user['mail']): + groups.append(self.groups.filter(name=STAFF_GROUP_NAME).first()) + + roles = [role for role in user.get('roles').values() if role != "authenticated user"] + + for role in roles: + group_name = role_map.get(role) + if group_name: + groups.append(self.groups.filter(name=group_name).first()) + + return groups + + def get_email(self, user, anonymize=False): + email = user['mail'] + if not anonymize or self.is_staff(email): + return email + + return f"aeon+{user['uid']}@torchbox.com" + + def is_staff(self, email): + _, email_domain = email.split('@') + return email_domain in settings.STAFF_EMAIL_DOMAINS diff --git a/opentech/apply/users/migrations/0005_user_drupal_id.py b/opentech/apply/users/migrations/0005_user_drupal_id.py new file mode 100644 index 0000000000000000000000000000000000000000..662c55217d96a415dcca24a8a5c7564c788a9534 --- /dev/null +++ b/opentech/apply/users/migrations/0005_user_drupal_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.8 on 2018-02-28 15:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_drop_first_last_names'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='drupal_id', + field=models.IntegerField(blank=True, editable=False, null=True), + ), + ] diff --git a/opentech/apply/users/models.py b/opentech/apply/users/models.py index 9e867ab9b60c9d430a1e0f79825514b20d043249..f4d22666087e3159ec7d4266bb9513d6b4f591ef 100644 --- a/opentech/apply/users/models.py +++ b/opentech/apply/users/models.py @@ -48,6 +48,9 @@ class User(AbstractUser): email = models.EmailField(_('email address'), unique=True) full_name = models.CharField(verbose_name='Full name', max_length=255, blank=True) + # Meta: used for migration purposes only + drupal_id = models.IntegerField(null=True, blank=True, editable=False) + USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] diff --git a/opentech/public/navigation/templates/navigation/primarynav-apply.html b/opentech/public/navigation/templates/navigation/primarynav-apply.html new file mode 100644 index 0000000000000000000000000000000000000000..00bc9cb9fc518182ce18ecd769c83392acf3173d --- /dev/null +++ b/opentech/public/navigation/templates/navigation/primarynav-apply.html @@ -0,0 +1,19 @@ +<nav role="navigation" aria-label="Primary"> + <ul class="nav nav--primary" role="menubar"> + <li class="nav__item" role="presentation"> + <a class="nav__link" href="{% url "dashboard:dashboard" %}" role="menuitem"> + Dashboard + </a> + </li> + <li class="nav__item" role="presentation"> + <a class="nav__link" href="{% url "funds:submissions" %}" role="menuitem"> + Submissions + </a> + </li> + </ul> +</nav> +<a href="#" class="link link--button-transparent link--mobile-standout"> + <svg class="icon"><use xlink:href="#person-icon"></use></svg> + My OTF +</a> +<a href="{% url 'users:logout' %}" class="link link--button-transparent link--mobile-standout">Logout</a> diff --git a/opentech/static_src/src/images/speech-bubble-blue.svg b/opentech/static_src/src/images/speech-bubble-blue.svg new file mode 100644 index 0000000000000000000000000000000000000000..7cf9ae0351151f904bb91bcbe35f99d93538575c --- /dev/null +++ b/opentech/static_src/src/images/speech-bubble-blue.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 18 19" xmlns="http://www.w3.org/2000/svg"><path d="M8.573 16.715l-3.32 2.007a1 1 0 0 1-1.518-.856v-2.545A8.358 8.358 0 0 1 8.358 0H9.47a8.358 8.358 0 0 1 0 16.715h-.898z" fill="#25aae1" fill-rule="evenodd"/></svg> diff --git a/opentech/static_src/src/javascript/components/mobile-menu.js b/opentech/static_src/src/javascript/components/mobile-menu.js index 56de75c65b265ae429dbad33bca81c7f35063f9b..6abe15a8eeceddf56c6ae2d4b16f4e899239fa3c 100644 --- a/opentech/static_src/src/javascript/components/mobile-menu.js +++ b/opentech/static_src/src/javascript/components/mobile-menu.js @@ -21,14 +21,17 @@ class MobileMenu { // toggle mobile menu this.mobileMenu[0].classList.toggle('is-visible'); - // reset the search whenever the mobile menu is toggled - if(this.search[0].classList.contains('is-visible')){ - this.search[0].classList.toggle('is-visible'); - document.querySelector('.header__inner--menu-open').classList.toggle('header__inner--search-open'); + // check if search exists + if (document.body.contains(this.search[0])) { + // reset the search whenever the mobile menu is toggled + if(this.search[0].classList.contains('is-visible')){ + this.search[0].classList.toggle('is-visible'); + document.querySelector('.header__inner--menu-open').classList.toggle('header__inner--search-open'); + } } // reset the search show/hide icons - if(this.mobileMenu[0].classList.contains('is-visible')){ + if(this.mobileMenu[0].classList.contains('is-visible') && document.body.contains(this.search[0])){ document.querySelector('.header__icon--open-search-menu-closed').classList.remove('is-hidden'); document.querySelector('.header__icon--close-search-menu-closed').classList.remove('is-unhidden'); } diff --git a/opentech/static_src/src/javascript/components/tabs.js b/opentech/static_src/src/javascript/components/tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..9f6bfcd555c302159d5dde88e517900f7e55901b --- /dev/null +++ b/opentech/static_src/src/javascript/components/tabs.js @@ -0,0 +1,72 @@ +class Tabs { + static selector() { + return '.js-tabs'; + } + + constructor() { + // The tabs + this.tabItems = Array.prototype.slice.call(document.querySelectorAll('.tab__item')); + + // The tabs content + this.tabsContents = Array.prototype.slice.call(document.querySelectorAll('.tabs__content')); + + // Active classes + this.tabActiveClass = 'tab__item--active'; + this.tabContentActiveClass = 'tabs__content--current'; + this.bindEvents(); + } + + bindEvents() { + // Get the current url + const url = document.location.toString(); + + // If the url contains a hash, activate the relevant tab + if (url.match('#')) { + this.updateTab(url); + } + + this.tabItems.forEach((el) => { + el.addEventListener('click', (e) => { + // prevent the page jumping + e.preventDefault(); + this.tabs(e); + }); + }); + } + + updateTab(url) { + this.stripTabClasses(); + + // Find tab with matching hash and activate + const match = document.querySelector(`a[href="#${url.split('#')[1]}"]`); + const tabId = match.getAttribute('data-tab'); + + this.addTabClasses(match, tabId); + } + + tabs(e) { + this.stripTabClasses(); + + // Find current tab + const tab = e.currentTarget; + + // Tab id is set in data-tab in html + const tabId = tab.getAttribute('data-tab'); + + this.addTabClasses(tab, tabId); + } + + stripTabClasses(){ + // remove active classes from all tabs and tab contents + this.tabItems.forEach(tabItem => tabItem.classList.remove(this.tabActiveClass)); + this.tabsContents.forEach(tabsContent => tabsContent.classList.remove(this.tabContentActiveClass)); + } + + addTabClasses(tab, tabId){ + // add active classes to tabs and their respecitve content + tab.classList.add(this.tabActiveClass); + document.querySelector(`#${tabId}`).classList.add(this.tabContentActiveClass); + } +} + +export default Tabs; diff --git a/opentech/static_src/src/javascript/main.js b/opentech/static_src/src/javascript/main.js index 5ccb9129086393b1d9929fe7a5b49890ccaf2a33..fbf5b7f485298e3b5e442023da53398c0116df2c 100755 --- a/opentech/static_src/src/javascript/main.js +++ b/opentech/static_src/src/javascript/main.js @@ -2,6 +2,7 @@ import jQuery from './globals'; import MobileMenu from './components/mobile-menu'; import Search from './components/search'; import MobileSearch from './components/mobile-search'; +import Tabs from './components/tabs'; import '@fancyapps/fancybox'; (function ($) { @@ -21,6 +22,10 @@ import '@fancyapps/fancybox'; new MobileSearch($(el), $('.header__menus--mobile'), $('.header__search'), $('.js-search-toggle')); }); + $(Tabs.selector()).each((index, el) => { + new Tabs($(el)); + }); + // Show list of selected files for upload on input[type=file] $('input[type=file]').change(function() { // remove any existing files first @@ -44,6 +49,28 @@ import '@fancyapps/fancybox'; animationDuration : 350, animationEffect : 'fade' }); + + // Open the activity feed + $('.js-open-feed').click((e) => { + e.preventDefault(); + $('body').addClass('no-scroll'); + $('.js-activity-feed').addClass('is-open'); + }); + + // Close the activity feed + $('.js-close-feed').click((e) => { + e.preventDefault(); + $('body').removeClass('no-scroll'); + $('.js-activity-feed').removeClass('is-open'); + }); + + // Show scroll to top of activity feed button on scroll + $('.js-activity-feed').on('scroll', function() { + $(this).scrollTop() === 0 ? $('.js-to-top').removeClass('is-visible') : $('.js-to-top').addClass('is-visible'); + }); + + // Scroll to the top of the activity feed + $('.js-to-top').click(() => $('.js-activity-feed').animate({ scrollTop: 0 }, 250)); }); // Add active class to filters - dropdowns are dynamically appended to the dom, diff --git a/opentech/static_src/src/sass/apply/abstracts/_variables.scss b/opentech/static_src/src/sass/apply/abstracts/_variables.scss index bb0d5610d1168c368e8804aefd100eea2c546098..1b2510ba1d4f8e4c14fefb64d74816e45d65ec0b 100755 --- a/opentech/static_src/src/sass/apply/abstracts/_variables.scss +++ b/opentech/static_src/src/sass/apply/abstracts/_variables.scss @@ -17,7 +17,8 @@ $color--pink: #e35ca6; $color--light-pink: #ffe1df; $color--tomato: #f05e54; $color--mint: #40c2ad; - +$color--grass: #7dc588; +$color--ocean: #1888b1; $color--sky-blue: #e7f2f6; $color--marine: #177da8; diff --git a/opentech/static_src/src/sass/apply/components/_activity-feed.scss b/opentech/static_src/src/sass/apply/components/_activity-feed.scss new file mode 100644 index 0000000000000000000000000000000000000000..1b5ffeaafd5c9428de918cd39283fdcd925fb6c8 --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_activity-feed.scss @@ -0,0 +1,40 @@ +.activity-feed { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + opacity: 0; + transition: opacity, z-index, $transition; + + &.is-open { + z-index: 20; + width: 100%; + height: 100vh; + max-height: 100%; + overflow: scroll; + background: $color--white; + opacity: 1; + } + + &__header { + padding: 0 20px; + background-color: $color--default; + + @include media-query(tablet-portrait) { + padding: 0; + } + } + + &__heading { + display: flex; + align-items: center; + padding: 20px 0; + color: $color--white; + + @include media-query(tablet-portrait) { + justify-content: center; + } + } +} diff --git a/opentech/static_src/src/sass/apply/components/_comment.scss b/opentech/static_src/src/sass/apply/components/_comment.scss new file mode 100644 index 0000000000000000000000000000000000000000..d09963a1b4f19dacc576c42105ed6ef3f1ca40af --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_comment.scss @@ -0,0 +1,33 @@ +.comment { + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px solid $color--light-mid-grey; + + @include media-query(tablet-portrait) { + max-width: 60%; + } + + &:first-of-type { + margin-top: 20px; + } + + &:last-of-type { + padding-bottom: 0; + margin-bottom: 0; + border-bottom: 0; + } + + &__time, + &__user, + &__copy { + margin: 0; + } + + &__time { + font-size: map-get($font-sizes, milli); + } + + &__user { + font-weight: $weight--semibold; + } +} diff --git a/opentech/static_src/src/sass/apply/components/_feed.scss b/opentech/static_src/src/sass/apply/components/_feed.scss new file mode 100644 index 0000000000000000000000000000000000000000..df79653ca8e125bd8c1bf017df31226dca6e7f06 --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_feed.scss @@ -0,0 +1,155 @@ +.feed { + &__item { + position: relative; + display: flex; + padding-bottom: 20px; + margin-bottom: 25px; + border-bottom: 1px solid $color--mid-grey; + + &:last-child { + border-bottom: 0; + } + } + + &__label { + padding: 5px 10px; + margin: 0; + font-size: 12px; + font-weight: $weight--bold; + color: $color--white; + text-align: center; + + &--note { + background-color: $color--error; + } + + &--determination { + background-color: $color--ocean; + } + + &--comment, + &--message { + background-color: $color--grass; + } + + &--activity, + &--action { + background-color: $color--mint; + } + + &--mobile { + display: block; + margin-right: 10px; + + @include media-query(small-tablet) { + display: none; + } + } + } + + &__pre-content { + display: none; + width: 110px; + + @include media-query(small-tablet) { + display: block; + } + } + + &__content { + @include media-query(small-tablet) { + width: 70%; + padding-left: 20px; + } + } + + &__meta { + @include responsive-font-sizes(12px, 16px); + display: flex; + align-items: center; + flex-wrap: wrap; + text-transform: uppercase; + + @include media-query(small-tablet) { + margin-bottom: 10px; + } + } + + &__meta-item { + margin: 0 10px 0 0; + + @include media-query(small-tablet) { + margin: 0 15px 0 0; + } + + &--progress { + width: 100%; + margin: 5px 0 0; + font-size: map-get($font-sizes, milli);; + color: $color--black-50; + + @include media-query(small-tablet) { + width: auto; + margin: 0 20px 0 0; + } + + span { + &:first-child { + &::after { + @include triangle(right, $color--black-50, 4px); + display: inline-block; + margin: 0 5px 0 10px; + } + } + } + } + } + + &__heading { + display: flex; + align-items: center; + flex-wrap: wrap; + + > * { + margin: 0 10px 0 0; + } + } + + &__heading-status { + padding: 3px 10px; + font-size: map-get($font-sizes, milli); + font-weight: $weight--bold; + color: $color--marine; + text-align: center; + text-transform: uppercase; + background-color: $color--sky-blue; + } + + &__teaser { + display: none; + margin: 10px 0 0; + color: $color--black-50; + + @include media-query(small-tablet) { + display: block; + } + } + + &__name { + width: 100%; + margin: 5px 0 10px; + + .feed__item--opened & { + color: $color--black-50; + } + + @include media-query(small-tablet) { + width: auto; + margin: 0 10px 0 0; + } + } + + &__company { + @extend %h6; + } +} diff --git a/opentech/static_src/src/sass/apply/components/_form.scss b/opentech/static_src/src/sass/apply/components/_form.scss index 66a85909838f0a239fd8a4e7fc32eb801b230e1c..4f1b5c273931fe890be2ef1d8d22252152f4721c 100644 --- a/opentech/static_src/src/sass/apply/components/_form.scss +++ b/opentech/static_src/src/sass/apply/components/_form.scss @@ -257,5 +257,19 @@ background: $color--light-pink; border: 1px solid $color--tomato; } + + textarea, + &__textarea { + display: block; + width: 100%; + height: 100px; + padding: 10px; + margin-bottom: 20px; + border: 1px solid $color--mid-grey; + + @include media-query(tablet-portrait) { + max-width: 70%; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_heading.scss b/opentech/static_src/src/sass/apply/components/_heading.scss index 022424e5470786d80f7ccf2886c8e842bcd6e293..801da1b54879967bddb493385a916853715f6d3a 100644 --- a/opentech/static_src/src/sass/apply/components/_heading.scss +++ b/opentech/static_src/src/sass/apply/components/_heading.scss @@ -45,4 +45,20 @@ &--uppercase { text-transform: uppercase; } + + + &--normal { + font-weight: $weight--normal; + } + + &--activity-feed { + @include responsive-font-sizes(12px, 18px); + line-height: 1.1; + text-align: center; + + @include media-query(tablet-portrait) { + line-height: auto; + text-align: left; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_icon.scss b/opentech/static_src/src/sass/apply/components/_icon.scss index 8688dfbadf1e42d1c56dddb66d84b52b773891b2..fd6e216b5e96e394a52d2cb987acb7e1abb9d47d 100644 --- a/opentech/static_src/src/sass/apply/components/_icon.scss +++ b/opentech/static_src/src/sass/apply/components/_icon.scss @@ -51,4 +51,39 @@ width: 14px; height: 14px; } + + &--speech-bubble { + fill: $color--white; + + .activity-feed__header & { + margin-left: 10px; + } + } + + &--to-top { + display: flex; + align-items: center; + width: 45px; + height: 45px; + padding: 12px; + margin: 0 0 5px; + background-color: $color--white; + border: 2px solid $color--black-50; + border-radius: 50%; + fill: $color--black-50; + + @include media-query(small-tablet) { + width: 65px; + height: 65px; + padding: 20px; + } + } + + &--private-eye { + position: absolute; + top: 0; + right: 0; + width: 35px; + height: 25px; + } } diff --git a/opentech/static_src/src/sass/apply/components/_link.scss b/opentech/static_src/src/sass/apply/components/_link.scss index 290892a1cf55fe3c5de3e19a8a664d44de899d0a..dd5c44209dccc01a89a9a2aaf010f809921ec11c 100644 --- a/opentech/static_src/src/sass/apply/components/_link.scss +++ b/opentech/static_src/src/sass/apply/components/_link.scss @@ -23,6 +23,7 @@ border-bottom: 1px solid $color--mid-grey; } + &:focus, &:hover { background-color: transparentize($color--light-blue, 0.9); } @@ -47,4 +48,120 @@ } } } + + &--mobile-standout { + display: block; + width: 100%; + max-width: 250px; + margin: 1rem auto 0; + font-weight: $weight--bold; + text-align: center; + + @include media-query(tablet-portrait) { + display: none; + } + } + + &--button-transparent { + @include button(transparent, $color--darkest-blue); + color: $color--white; + + &:focus, + &:hover { + border: 1px solid transparent; + } + } + + &--open-feed { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 10; + display: flex; + align-items: center; + width: 60px; + height: 60px; + color: $color--white; + background: url('./../../images/speech-bubble-blue.svg') no-repeat center; + + @include media-query(tablet-portrait) { + @include button($color--light-blue, $color--dark-blue); + right: 5%; + bottom: 0; + width: auto; + height: auto; + padding: 8px 20px; + background: $color--light-blue; + border: 0; + + &::after { + width: 30px; + height: 30px; + margin-left: 30px; + font-size: 30px; + line-height: 0.9; + text-align: center; + border: 2px solid white; + border-radius: 50%; + content: '+'; + } + } + + @include media-query(tablet-landscape) { + right: 10%; + } + } + + &--close-feed { + position: absolute; + top: 20px; + right: 0; + display: flex; + align-items: center; + font-size: 12px; + font-weight: 700; + color: $color--white; + text-transform: uppercase; + + @include media-query(tablet-portrait) { + top: 25px; + } + + &::after { + width: 30px; + height: 30px; + margin-left: 10px; + font-size: 30px; + line-height: 0.1; + text-align: center; + border: 2px solid white; + border-radius: 50%; + content: '_'; + + @include media-query(tablet-portrait) { + margin-left: 20px; + } + } + } + + &--to-top { + position: fixed; + right: 20px; + bottom: 0; + display: flex; + align-items: center; + flex-direction: column; + color: $color--black-50; + opacity: 0; + transition: opacity $transition; + + @include media-query(tablet-portrait) { + right: 30px; + bottom: 20px; + } + + &.is-visible { + opacity: 1; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_nav.scss b/opentech/static_src/src/sass/apply/components/_nav.scss new file mode 100644 index 0000000000000000000000000000000000000000..243cd36fd3774537f83b91a568592dee6e42b94e --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_nav.scss @@ -0,0 +1,77 @@ +.nav { + display: flex; + align-items: center; + flex-direction: column; + width: 100%; + height: 100%; + font-weight: $weight--semibold; + + @include media-query(tablet-portrait) { + flex-direction: row; + padding-top: 0; + } + + &--primary { + text-transform: uppercase; + + @include media-query(tablet-portrait) { + flex-direction: row; + justify-content: center; + margin-top: 0; + text-transform: none; + } + } + + &__item { + @include media-query(tablet-portrait) { + margin-right: 75px; + + &:last-child { + margin-right: 0; + } + } + } + + &__link { + @extend %h5; + position: relative; + display: inline-block; + padding: 20px 10px; + color: $color--white; + transition: color $transition; + + &:focus, + &:hover { + color: $color--light-blue; + } + + @include media-query(tablet-portrait) { + display: inline; + padding: 30px 0; + color: $color--default; + } + + + &--active { + &::after { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 5px; + background-color: $color--dark-blue; + content: ''; + opacity: 0; + transition: opacity 0.5s cubic-bezier(0.2, 1.4, 0.67, 1.13); + } + } + + &--active, + &:focus, + &:hover { + &::after { + opacity: 1; + } + } + } +} diff --git a/opentech/static_src/src/sass/apply/components/_status-bar.scss b/opentech/static_src/src/sass/apply/components/_status-bar.scss index 532c1569841e1140ba63af8baf3412c255da6949..f47f20dddf87d71230db88ae2aed25b3e843b77e 100644 --- a/opentech/static_src/src/sass/apply/components/_status-bar.scss +++ b/opentech/static_src/src/sass/apply/components/_status-bar.scss @@ -13,11 +13,19 @@ } } + &--small { + width: 100%; + max-width: 800px; + margin-right: 40px; + color: $color--white; + } + &__subheading { display: inline-block; padding: 5px 10px; margin: 10px 0 0; - background: $color--tomato; + color: $color--white; + background-color: $color--tomato; } &__icon { @@ -29,8 +37,15 @@ width: 20px; height: 20px; + .status-bar__item--is-current &, .status-bar__item--is-complete & { display: block; + + .status-bar--small & { + display: block; + border-radius: 50%; + box-shadow: 0 1px 9px 0 $color--black-50; + } } .status-bar__item:first-of-type & { @@ -55,6 +70,10 @@ border: 5px solid $color--mid-grey; border-radius: 50%; content: ''; + + .status-bar--small & { + background: $color--white; + } } // last items dont have a dot @@ -102,6 +121,10 @@ &::before { background: $color--primary; border-color: $color--primary; + + .status-bar--small & { + background: $color--primary; + } } } } @@ -113,6 +136,7 @@ z-index: 100; width: 20px; height: 20px; + border-radius: 50%; opacity: 0; transition: opacity $transition; @@ -144,7 +168,7 @@ display: block; padding: 5px 10px; font-size: 12px; - font-weight: $weight--semibold; + font-weight: $weight--bold; background-color: $color--error; content: attr(data-title); diff --git a/opentech/static_src/src/sass/apply/components/_tabs.scss b/opentech/static_src/src/sass/apply/components/_tabs.scss new file mode 100644 index 0000000000000000000000000000000000000000..eae2667a846ba9d86ed7cf967c653e77e76d74ee --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_tabs.scss @@ -0,0 +1,51 @@ +.tabs { + margin: 20px 0 -20px; + + &__container { + display: flex; + } + + &__content { + display: none; + + &--current { + display: inherit; + } + + } +} + +.tab__item { + @include responsive-font-sizes(12px, 15px); + position: relative; + width: 33%; + padding: 7px; + font-weight: $weight--bold; + hyphens: auto; + color: $color--mid-grey; + text-transform: uppercase; + transition: color, background-color, 0.1s ease-out; + + @include media-query(mob-landscape) { + padding: 15px; + } + + @include media-query(small-tablet) { + width: auto; + padding: 20px; + } + + &:hover { + color: $color--light-blue; + } + + &--active { + color: $color--default; + cursor: default; + background-color: $color--white; + + &:hover { + background-color: $color--white; + } + } +} diff --git a/opentech/static_src/src/sass/apply/components/_wrapper.scss b/opentech/static_src/src/sass/apply/components/_wrapper.scss index bec7f413234b8d77207b241103e3a82a449d8423..3b93fa4f6ba13a7141efaff90caf7bd94b434dc8 100644 --- a/opentech/static_src/src/sass/apply/components/_wrapper.scss +++ b/opentech/static_src/src/sass/apply/components/_wrapper.scss @@ -123,6 +123,10 @@ @include media-query(mob-landscape) { padding: 2rem 0; } + + + .wrapper--inner-space-medium { + padding: 0; + } } &--inner-space-large { @@ -238,4 +242,66 @@ justify-content: space-between; margin-bottom: 20px; } + + &--apply-box { + width: 385px; + padding: 20px; + color: $color--default; + background-color: $color--white; + } + + &--applicant-dashboard { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + @include media-query(tablet-portrait) { + padding: 20px; + } + } + + &--status-bar { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: 20px; + background-color: $color--white; + border-bottom: 3px solid $color--light-grey; + + div:first-child { + padding-right: 20px; + } + } + + &--relative { + position: relative; + } + + &--activity-feed { + padding: 0 20px; + margin-top: 50px; + + @include media-query(tablet-landscape) { + margin-top: 70px; + } + } + + &--comments { + padding-bottom: 15px; + margin-bottom: 15px; + border-bottom: 1px solid $color--mid-grey; + + @include media-query(tablet-portrait) { + padding-bottom: 35px; + margin-bottom: 35px; + } + } + + &--tabs { + padding: 20px 0; + + @include media-query(tablet-portrait) { + padding: 4rem 0; + } + } } diff --git a/opentech/static_src/src/sass/apply/layout/_header.scss b/opentech/static_src/src/sass/apply/layout/_header.scss index c5255772460b7f5a0b61b248926c3a066e778e96..ca109124d3d179367c29583a5e464204335c648d 100644 --- a/opentech/static_src/src/sass/apply/layout/_header.scss +++ b/opentech/static_src/src/sass/apply/layout/_header.scss @@ -11,6 +11,10 @@ justify-content: space-between; width: 100%; + &--menu-open { + padding: 20px; + } + &--mobile-buttons { justify-content: flex-end; @@ -27,6 +31,10 @@ width: 60px; height: 60px; + .is-visible & { + fill: $color--white; + } + @include media-query(tablet-landscape) { display: none; } @@ -37,8 +45,8 @@ @include media-query(tablet-landscape) { display: block; - width: 215px; - height: 50px; + width: 160px; + height: 40px; } } } @@ -62,4 +70,46 @@ display: flex; } } + + &__menus { + flex-grow: 1; + + &--desktop { + display: none; + + @include media-query(tablet-portrait) { + display: flex; + align-items: center; + justify-content: center; + } + } + + &--mobile { + position: fixed; + top: 0; + left: 0; + z-index: 10; + width: 100%; + height: 100%; + background: $color--dark-grey; + opacity: 0; + transform: translate3d(0, -100%, 0); + transition-duration: 0.25s; + transition-property: transform, opacity; + transition-timing-function: cubic-bezier(0.65, 0.05, 0.36, 1); + + &.is-visible { + opacity: 1; + transform: translate3d(0, 0%, 0); + + @include media-query(tablet-portrait) { + display: none; + } + } + + nav { + width: 100%; + } + } + } } diff --git a/opentech/static_src/src/sass/apply/main.scss b/opentech/static_src/src/sass/apply/main.scss index dbc1c9ffd4030b3f74491f5983b6eb5b8904197d..d50e04f7b45db1a3fe274611b2033446adf12f62 100755 --- a/opentech/static_src/src/sass/apply/main.scss +++ b/opentech/static_src/src/sass/apply/main.scss @@ -12,7 +12,10 @@ @import 'base/typography'; // Components +@import 'components/activity-feed'; +@import 'components/comment'; @import 'components/button'; +@import 'components/feed'; @import 'components/grid'; @import 'components/heading'; @import 'components/icon'; @@ -21,11 +24,13 @@ @import 'components/rich-text'; @import 'components/section'; @import 'components/sidebar'; +@import 'components/tabs'; @import 'components/status-bar'; @import 'components/form'; @import 'components/heading'; @import 'components/icon'; @import 'components/input'; +@import 'components/nav'; @import 'components/pagination'; @import 'components/select2'; @import 'components/table'; diff --git a/opentech/templates/base-apply.html b/opentech/templates/base-apply.html index feab81bcbb5d6c6e5b1bc21f7482a9d1f572fab5..9bdf32fde254ad0c96789ef0442885cc1ea1371d 100644 --- a/opentech/templates/base-apply.html +++ b/opentech/templates/base-apply.html @@ -55,6 +55,24 @@ </button> </div> + <section class="header__menus header__menus--desktop"> + {% include "navigation/primarynav-apply.html" %} + </section> + + <section class="header__menus header__menus--mobile"> + <div class="header__inner header__inner--menu-open"> + <a href="{% slugurl 'home' %}" aria-label="Home link"> + <svg class="header__logo header__logo--mobile"><use xlink:href="#logo-mobile"></use></svg> + </a> + <div class="header__inner header__inner--mobile-buttons"> + <button class="button button--left-space js-mobile-menu-close"> + <svg class="header__icon header__icon--cross icon icon--mobile-menu"><use xlink:href="#cross"></use></svg> + </button> + </div> + </div> + {% include "navigation/primarynav-apply.html" %} + </section> + <div class="header__button-container"> <a href="#" class="button button--transparent button--narrow button--contains-icons"> <svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg> diff --git a/opentech/templates/includes/sprites.html b/opentech/templates/includes/sprites.html index c1264e9d8e60b127b7a6a8b7015446d92ecda426..f4ba294233bf4beb42819f4c5f540381031687cb 100644 --- a/opentech/templates/includes/sprites.html +++ b/opentech/templates/includes/sprites.html @@ -211,7 +211,7 @@ <path d="M8.176 13.833V2.167M8.303 14l4.991-4.714M8 14L3.009 9.286M13.824 19.5H2.176" /> </g> </symbol> - + <symbol id="wifi" viewBox="0 0 69 42"> <g stroke-width="6" fill="none" fill-rule="evenodd"> <path d="M2 16.365c20.786-17.82 42.253-17.82 64.402 0M15 27.842c12.195-10.456 24.79-10.456 37.785 0M27 39c4.666-4 9.484-4 14.456 0" /> @@ -269,4 +269,21 @@ <circle transform="rotate(90 4.8 2.24)" cx="4.8" cy="2.24" r="2.24" /> </g> </symbol> + + <symbol id="speech-bubble" viewBox="0 0 18 19"> + <path d="M8.573 16.715l-3.32 2.007a1 1 0 0 1-1.518-.856v-2.545A8.358 8.358 0 0 1 8.358 0H9.47a8.358 8.358 0 0 1 0 16.715h-.898z" fill-rule="evenodd"/> + </symbol> + + <symbol id="chevron" viewBox="0 0 23 12"> + <path d="M11.5 0l-.975.824L0 9.737 1.95 12l9.55-8.089L21.05 12 23 9.737 12.475.824z" fill-rule="nonzero" /> + </symbol> + + <symbol id="private-eye" viewBox="0 0 29 21"> + <g fill="none" fill-rule="evenodd"> + <path d="M14.5 13.976c1.883 0 3.41-1.556 3.41-3.476s-1.527-3.476-3.41-3.476c-1.883 0-3.41 1.556-3.41 3.476s1.527 3.476 3.41 3.476z" stroke="#F05E54" stroke-width="2" /> + <path d="M17.32 8.366c-1.234-1.47-3.404-1.682-4.846-.472-1.442 1.21-1.611 3.384-.378 4.854" fill="#F05E54" /> + <path d="M14.5 19.073c4.206.017 8.337-2.918 12.393-8.805-3.324-5.56-7.455-8.341-12.393-8.341-4.938 0-9.082 2.858-12.432 8.573 4.172 5.699 8.316 8.556 12.432 8.573z" stroke="#F05E54" stroke-width="2" /> + <path d="M24.273 1L5.182 20" stroke="#F05E54" stroke-width="2" stroke-linecap="round" stroke-linejoin="bevel" /> + </g> + </symbol> </svg>