diff --git a/hypha/public/partner/__init__.py b/hypha/public/partner/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hypha/public/partner/admin.py b/hypha/public/partner/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..7f74539a7f82ddee1ecd4c9ee43031ba440af398 --- /dev/null +++ b/hypha/public/partner/admin.py @@ -0,0 +1,21 @@ +from wagtail.contrib.modeladmin.options import ( + ModelAdmin, + modeladmin_register, +) +from .models import Investment + + +class InvestmentAdmin(ModelAdmin): + model = Investment + form_fields_exclude = ('application', ) + menu_label = 'Investments' + menu_icon = 'placeholder' + menu_order = 290 + add_to_settings_menu = False + exclude_from_explorer = False + list_display = ('partner', 'name', 'amount_committed', 'year') + list_filter = ('partner__name', 'year', 'amount_committed') + search_fields = ('name', 'year') + + +modeladmin_register(InvestmentAdmin) diff --git a/hypha/public/partner/apps.py b/hypha/public/partner/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..01010fa868e13eb9a79249966026aadf996412cd --- /dev/null +++ b/hypha/public/partner/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PartnerConfig(AppConfig): + name = 'partner' diff --git a/hypha/public/partner/migrations/0001_add_investments_table_and_partners_page.py b/hypha/public/partner/migrations/0001_add_investments_table_and_partners_page.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ac3b68d1429d4f9bb76391488d2e9c598cebdd --- /dev/null +++ b/hypha/public/partner/migrations/0001_add_investments_table_and_partners_page.py @@ -0,0 +1,75 @@ +# Generated by Django 2.2.16 on 2020-09-13 22:41 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import hypha.public.partner.models +import wagtail.core.fields +import wagtailcache.cache + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('images', '0003_customimage_drupal_id'), + ('wagtailcore', '0045_assign_unlock_grouppagepermission'), + ('funds', '0078_add_heading_block_to_form_fields_block'), + ] + + operations = [ + migrations.CreateModel( + name='PartnerPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('social_text', models.CharField(blank=True, max_length=255)), + ('listing_title', models.CharField(blank=True, help_text='Override the page title used when this page appears in listings', max_length=255)), + ('listing_summary', models.CharField(blank=True, help_text="The text summary used when this page appears in listings. It's also used as the description for search engines if the 'Search description' field above is not defined.", max_length=255)), + ('name', models.CharField(max_length=100)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive')], default='current_partner', max_length=20)), + ('public', models.BooleanField(default=True)), + ('description', wagtail.core.fields.RichTextField(blank=True)), + ('web_url', models.URLField(blank=True)), + ('header_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ('listing_image', models.ForeignKey(blank=True, help_text='Choose the image you wish to be displayed when this page appears in listings', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ('logo', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ('social_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ], + options={ + 'verbose_name': 'Partner Page', + }, + bases=(wagtailcache.cache.WagtailCacheMixin, 'wagtailcore.page', models.Model), + ), + migrations.CreateModel( + name='PartnerIndexPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('social_text', models.CharField(blank=True, max_length=255)), + ('listing_title', models.CharField(blank=True, help_text='Override the page title used when this page appears in listings', max_length=255)), + ('listing_summary', models.CharField(blank=True, help_text="The text summary used when this page appears in listings. It's also used as the description for search engines if the 'Search description' field above is not defined.", max_length=255)), + ('introduction', models.TextField(blank=True)), + ('header_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ('listing_image', models.ForeignKey(blank=True, help_text='Choose the image you wish to be displayed when this page appears in listings', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ('social_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')), + ], + options={ + 'abstract': False, + }, + bases=(wagtailcache.cache.WagtailCacheMixin, 'wagtailcore.page', models.Model), + ), + migrations.CreateModel( + name='Investment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('year', models.IntegerField(default=hypha.public.partner.models.current_year, help_text='Use format: <YYYY>', validators=[django.core.validators.MinValueValidator(1984), hypha.public.partner.models.max_value_current_year])), + ('amount_committed', models.DecimalField(decimal_places=2, default=0, max_digits=11, verbose_name='Ammount Commited US$')), + ('description', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now_add=True)), + ('application', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='funds.ApplicationSubmission')), + ('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='investments', to='partner.PartnerPage')), + ], + ), + ] diff --git a/hypha/public/partner/migrations/__init__.py b/hypha/public/partner/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hypha/public/partner/models.py b/hypha/public/partner/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b6457e4aaee3912a32dfd3cc39ae26167f1ef333 --- /dev/null +++ b/hypha/public/partner/models.py @@ -0,0 +1,120 @@ +import datetime + +from django.db import models +from django.core.validators import MaxValueValidator, MinValueValidator +from django.shortcuts import redirect + +from pagedown.widgets import PagedownWidget + +from wagtail.core.models import Page +from wagtail.core.fields import RichTextField +from wagtail.admin.edit_handlers import FieldPanel +from wagtail.search import index +from wagtail.images.edit_handlers import ImageChooserPanel + +from hypha.public.utils.models import BasePage +from hypha.apply.funds.models import ApplicationSubmission + + +class PartnerIndexPage(BasePage): + parent_page_types = ['standardpages.IndexPage'] + subpage_types = ['partner.PartnerPage'] + + introduction = models.TextField(blank=True) + + content_panels = BasePage.content_panels + [ + FieldPanel('introduction', widget=PagedownWidget()), + ] + + search_fields = BasePage.search_fields + [ + index.SearchField('introduction'), + ] + + def serve(self, request, *args, **kwargs): + # import ipdb; ipdb.set_trace() + return redirect('investments') + # return super().serve(request, *args, **kwargs) + + +class PartnerPage(BasePage): + STATUS = [ + ('active', 'Active'), + ('inactive', 'Inactive') + ] + + class Meta: + verbose_name = "Partner Page" + + parent_page_types = ['partner.PartnerIndexPage'] + subpage_types = [] + + name = models.CharField(max_length=100) + status = models.CharField( + choices=STATUS, default='current_partner', max_length=20 + ) + public = models.BooleanField(default=True) + description = RichTextField(blank=True) + web_url = models.URLField(blank=True) + logo = models.OneToOneField( + 'images.CustomImage', + null=True, + blank=True, + related_name='+', + on_delete=models.SET_NULL + ) + + content_panels = Page.content_panels + [ + FieldPanel('name'), + FieldPanel('status'), + FieldPanel('public'), + FieldPanel('description'), + FieldPanel('web_url'), + ImageChooserPanel('logo'), + ] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.url + + +def current_year(): + return datetime.date.today().year + + +def max_value_current_year(value): + return MaxValueValidator(current_year())(value) + + +class Investment(models.Model): + + partner = models.ForeignKey( + PartnerPage, + on_delete=models.CASCADE, + related_name='investments' + ) + name = models.CharField(max_length=50) + year = models.IntegerField( + default=current_year, + validators=[MinValueValidator(1984), max_value_current_year], + help_text='Use format: <YYYY>' + ) + amount_committed = models.DecimalField( + decimal_places=2, + default=0, + max_digits=11, + verbose_name='Ammount Commited US$' + ) + description = models.TextField() + application = models.OneToOneField( + ApplicationSubmission, + on_delete=models.SET_NULL, + blank=True, null=True + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name diff --git a/hypha/public/partner/tables.py b/hypha/public/partner/tables.py new file mode 100644 index 0000000000000000000000000000000000000000..58c04acd5d87532da1d86603f85ad6a9374628fc --- /dev/null +++ b/hypha/public/partner/tables.py @@ -0,0 +1,21 @@ +import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ + +from .models import Investment + + +class InvestmentTable(tables.Table): + """Table for listing investments.""" + partner = tables.Column(verbose_name='Partner name', linkify=True) + name = tables.Column(verbose_name='Investment') + year = tables.Column(verbose_name='Year') + status = tables.Column(accessor='partner__status', verbose_name='Status') + amount_committed = tables.Column(verbose_name='Amount committed (US$)') + + class Meta: + model = Investment + order_by = ('-updated_at',) + fields = ('partner', 'name', 'year', 'status', 'amount_committed') + template_name = 'partner/table.html' + attrs = {'class': 'all-investments-table'} + empty_text = _('No investments available') diff --git a/hypha/public/partner/templates/partner/base_investments_table.html b/hypha/public/partner/templates/partner/base_investments_table.html new file mode 100644 index 0000000000000000000000000000000000000000..71d970299eadd716e61bbf7accd02aeea543ad5c --- /dev/null +++ b/hypha/public/partner/templates/partner/base_investments_table.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% load static %} +{% load render_table from django_tables2 %} + +{% block content %} + {% block table %} + {% render_table table %} + {% endblock %} +{% endblock %} diff --git a/hypha/public/partner/templates/partner/investments.html b/hypha/public/partner/templates/partner/investments.html new file mode 100644 index 0000000000000000000000000000000000000000..0b1afe943e8a72ac23c077a33acf9566f17aeedb --- /dev/null +++ b/hypha/public/partner/templates/partner/investments.html @@ -0,0 +1,20 @@ +{% extends "partner/base_investments_table.html" %} +{% block title %}Investments{% endblock %} + +{% block content %} +<div class="admin-bar"> + <div class="admin-bar__inner wrapper--search"> + {% block page_header %} + <div> + <h1 class="gamma heading heading--no-margin heading--bold">Investments</h1> + </div> + {% endblock %} + </div> +</div> + +<div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% block table %} + {{ block.super }} + {% endblock %} +</div> +{% endblock %} diff --git a/hypha/public/partner/templates/partner/partner_page.html b/hypha/public/partner/templates/partner/partner_page.html new file mode 100644 index 0000000000000000000000000000000000000000..7d81860bdcc915526041f12e32df00df110e04ed --- /dev/null +++ b/hypha/public/partner/templates/partner/partner_page.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags wagtailimages_tags %} + +{% block content %} +<section class="wrapper wrapper--small wrapper--inner-space-large wrapper--sidebar"> + <div class="wrapper--sidebar--inner"> + <h4 class="heading">{{ page.name }}</h4> + + {{ page.description|richtext }} + + <p class="list list--contact"><span>Status: </span>{{ page.get_status_display }}</p> + + <p class="list list--contact"><span>Public: </span>{{ page.public|yesno:"Yes,No" }}</p> + + {% if page.web_url %} + <p class="list list--contact"><span>Website:</span> <a href="{{ page.web_url }}">{{ page.web_url }}</a></p> + {% endif %} + </div> + + <div> + {% if page.logo %} + {% image page.logo fill-210x235 class="image image--headshot-desktop" %} + {% endif %} + </div> +</section> +{% endblock %} diff --git a/hypha/public/partner/templates/partner/table.html b/hypha/public/partner/templates/partner/table.html new file mode 100644 index 0000000000000000000000000000000000000000..8780f70a655925491fb9e3036ee47fb15c258e36 --- /dev/null +++ b/hypha/public/partner/templates/partner/table.html @@ -0,0 +1,45 @@ +{% extends 'django_tables2/table.html' %} +{% load django_tables2 table_tags review_tags wagtailimages_tags i18n %} + +{% block table.tbody.row %} + <tr {{ row.attrs.as_html }}> + {% for column, cell in row.items %} + <td {{ column.attrs.td.as_html }}> + {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %} + </td> + {% endfor %} + </tr> + +{% endblock %} + +{% block table.tbody.empty_text %} +<tr class="all-investments-table__empty"><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr> +{% endblock table.tbody.empty_text %} + +{% block pagination %} + {% if table.page and table.paginator.num_pages > 1 %} + <ul class="pagination"> + {% if table.page.has_previous %} + <li class="previous"> + <a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"> + {% trans 'previous' %} + </a> + </li> + {% endif %} + {% if table.page.has_previous or table.page.has_next %} + <li class="cardinality"> + <p> + Page {{ table.page.number }} + </p> + </li> + {% endif %} + {% if table.page.has_next %} + <li class="next"> + <a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"> + {% trans 'next' %} + </a> + </li> + {% endif %} + </ul> + {% endif %} +{% endblock pagination %} diff --git a/hypha/public/partner/views.py b/hypha/public/partner/views.py new file mode 100644 index 0000000000000000000000000000000000000000..74576492243e00443d7857bfe45077ea87f7d161 --- /dev/null +++ b/hypha/public/partner/views.py @@ -0,0 +1,15 @@ +import django_tables2 as tables +from django_tables2.views import SingleTableMixin +from django_filters.views import FilterView +from django.views.generic import ListView +from django_tables2 import SingleTableView + +from .tables import InvestmentTable +from .models import Investment + + +class InvestmentTableView(SingleTableView): + model = Investment + table_class = InvestmentTable + table_pagination = {'per_page': 5} + template_name = 'partner/investments.html' diff --git a/hypha/public/people/templates/people/person_page.html b/hypha/public/people/templates/people/person_page.html index f6f605655f1c639264c935b87947a27a42b35f93..7ffbd3e8e446bbd8c36c5d40427a2bd3b995d113 100644 --- a/hypha/public/people/templates/people/person_page.html +++ b/hypha/public/people/templates/people/person_page.html @@ -45,15 +45,6 @@ {% if page.photo %} {% image page.photo fill-210x235 class="image image--headshot-desktop" %} {% endif %} - - {% if page.social_media_profile.all %} - <h5>Follow {{ page.first_name }}</h5> - {% for item in page.social_media_profile.all %} - <a aria-label="Social media link" href="{{ item.profile_url }}"> - <svg class="icon icon--social-share icon--{{item.service}}-share"><use xlink:href="#{{item.service}}"></use></svg> - </a> - {% endfor %} - {% endif %} </div> </section> {% endblock %} diff --git a/hypha/public/urls.py b/hypha/public/urls.py index a1868e486fe4e5b2079e4c954b1589e7f77da439..6e38187cf0f79523897a5133dc7b35e7e43bc2be 100644 --- a/hypha/public/urls.py +++ b/hypha/public/urls.py @@ -3,10 +3,12 @@ from django.urls import include, path from .mailchimp import urls as newsletter_urls from .news import feeds as news_feeds from .search import views as search_views +from .partner import views as partner_views urlpatterns = [ path('search/', search_views.search, name='search'), path('news/feed/', news_feeds.NewsFeed(), name='news_feed'), path('news/<int:news_type>/feed/', news_feeds.NewsTypeFeed(), name='news_type_feed'), - path('newsletter/', include(newsletter_urls)) + path('newsletter/', include(newsletter_urls)), + path('investments/', partner_views.InvestmentTableView.as_view(), name='investments') ] diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 207161c95714e25f4a5881e6692a1f19d7de3f6e..6d178d40230a32643fdfddc17e2047be07bc4562 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -93,6 +93,7 @@ INSTALLED_APPS = [ 'hypha.public.standardpages', 'hypha.public.forms', 'hypha.public.utils', + 'hypha.public.partner', 'social_django', diff --git a/hypha/static_src/src/sass/public/abstracts/_mixins.scss b/hypha/static_src/src/sass/public/abstracts/_mixins.scss index 1e47f7effcd180be25c0af62e0e0e1f1de39bbda..405db5f958bd5a07466c568a70f264ecb48d999d 100644 --- a/hypha/static_src/src/sass/public/abstracts/_mixins.scss +++ b/hypha/static_src/src/sass/public/abstracts/_mixins.scss @@ -176,3 +176,70 @@ border-bottom: $perpendicular-borders; } } + +@mixin table-ordering-styles { + thead { + th { + // ordering + &.desc, + &.asc { + position: relative; + color: $color--dark-grey; + + &::after { + position: absolute; + top: 50%; + margin-left: 3px; + } + + a { + color: inherit; + } + } + + &.desc { + &::after { + @include triangle(top, $color--default, 5px); + } + } + + &.asc { + &::after { + @include triangle(bottom, $color--default, 5px); + } + } + } + } +} + +@mixin column-scrolling { + @include media-query(tablet-landscape) { + height: calc(100vh - var(--header-admin-height) - #{$listing-header-height}); + overflow-y: scroll; + } + + @include media-query(laptop-short) { + // allow for vertical scrolling on laptops + height: calc(100vh - #{$listing-header-height}); + } +} + +@mixin table-checkbox { + input[type='checkbox'] { + margin: 0 auto; + display: block; + width: 20px; + height: 20px; + border: 1px solid $color--mid-grey; + -webkit-appearance: none; // sass-lint:disable-line no-vendor-prefixes + -moz-appearance: none; // sass-lint:disable-line no-vendor-prefixes + appearance: none; + background-color: $color--white; + + &:checked { + background: url('./../../images/tick.svg') $color--dark-blue center no-repeat; + background-size: 12px; + border: 1px solid $color--dark-blue; + } + } +} diff --git a/hypha/static_src/src/sass/public/abstracts/_variables.scss b/hypha/static_src/src/sass/public/abstracts/_variables.scss index 931be93eca8d30775535742d046caea9b5616222..4eba936258f93c97b387fdb4dfe67a5874a59145 100644 --- a/hypha/static_src/src/sass/public/abstracts/_variables.scss +++ b/hypha/static_src/src/sass/public/abstracts/_variables.scss @@ -128,3 +128,6 @@ $table-breakpoint: 'tablet-landscape'; // Dropdown height $dropdown-height: 45px; + +// Table breakpoint +$table-breakpoint: 'tablet-landscape'; diff --git a/hypha/static_src/src/sass/public/components/_table.scss b/hypha/static_src/src/sass/public/components/_table.scss new file mode 100644 index 0000000000000000000000000000000000000000..5bb173ed3f425c1ca9a4f557d5b2414287e5e4b6 --- /dev/null +++ b/hypha/static_src/src/sass/public/components/_table.scss @@ -0,0 +1,100 @@ +// base table styles - specific ones in their own scss partial +table { + width: 100%; + background-color: $color--white; + border-collapse: collapse; + table-layout: fixed; + + // table head + th { + padding: 20px 15px; + font-size: 15px; + font-weight: 600; + text-align: left; + + a { + color: $color--black-60; + transition: color .25s ease-out; + } + } + + // table rows + tr { + border: 1px solid $color--light-mid-grey; + transition: box-shadow .15s ease; + + @include media-query($table-breakpoint) { + border-top: 0; + border-right: 0; + border-bottom: 2px solid $color--light-grey; + border-left: 0; + + &.is-expanded { + border-bottom: 1px solid $color--light-mid-grey; + + .lead { + span { + background-color: $color--mist; + } + } + } + + &:hover { + box-shadow: 0 6px 35px -13px $color--black-50; + } + + &.reviews-summary__tr { + box-shadow: none; + } + } + + // responsive table styles + > td { + display: block; + width: 100%; + + @include media-query($table-breakpoint) { + display: table-cell; + width: initial; + } + + &.lead { + span { + @include media-query($table-breakpoint) { + position: relative; + z-index: 1; + display: block; + padding-right: 5px; + overflow: hidden; + text-overflow: ellipsis; + background: $color--white; + + &:hover { + display: inline-block; + overflow: visible; + } + } + } + } + + &.title { + a { + font-weight: $weight--bold; + } + } + } + } + + td, + th { + padding: 5px 20px; + + @include media-query($table-breakpoint) { + padding: 15px 10px; + } + + &.title { + padding-left: 20px; + } + } +} diff --git a/hypha/static_src/src/sass/public/main.scss b/hypha/static_src/src/sass/public/main.scss index d97cf885fba686b4a76a5d7975bec9887f61be01..4c56f2abf8552b72c7b276d9f0974ddcac9b697a 100644 --- a/hypha/static_src/src/sass/public/main.scss +++ b/hypha/static_src/src/sass/public/main.scss @@ -37,6 +37,7 @@ @import 'components/section'; @import 'components/select2'; @import 'components/show-more'; +@import 'components/table'; @import 'components/wrapper'; // Layout