├── apps ├── cc_cms │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests │ │ ├── __init__.py │ │ └── fixtures.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── section.py │ │ └── dynamic_text.py │ ├── apps.py │ ├── templates │ │ └── cc_cms │ │ │ ├── dynamic_text.html │ │ │ ├── page.html │ │ │ └── section.html │ ├── views.py │ ├── resources │ │ └── cc_cms.scss │ ├── urls.py │ ├── admin.py │ └── models.py └── fok │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ ├── 0002_user_is_greeted.py │ ├── 0003_auto_20190707_0417.py │ └── 0001_initial.py │ ├── tests │ ├── __init__.py │ └── fixtures.py │ ├── tests.py │ ├── static │ ├── styles │ │ └── scss │ │ │ ├── main.scss │ │ │ ├── definitions.scss │ │ │ ├── components │ │ │ ├── _icons-material-design.scss │ │ │ ├── _transitions.scss │ │ │ ├── forms │ │ │ │ ├── _forms.scss │ │ │ │ ├── _file-input.scss │ │ │ │ ├── _switches.scss │ │ │ │ ├── _radio-buttons.scss │ │ │ │ ├── _range.scss │ │ │ │ ├── _select.scss │ │ │ │ └── _checkboxes.scss │ │ │ ├── _pulse.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _table_of_contents.scss │ │ │ ├── _materialbox.scss │ │ │ ├── _color-classes.scss │ │ │ ├── _toast.scss │ │ │ ├── _badges.scss │ │ │ ├── _modal.scss │ │ │ ├── _dropdown.scss │ │ │ ├── _typography.scss │ │ │ ├── _slider.scss │ │ │ ├── _carousel.scss │ │ │ ├── _chips.scss │ │ │ ├── _tabs.scss │ │ │ ├── _collapsible.scss │ │ │ ├── _tapTarget.scss │ │ │ ├── _waves.scss │ │ │ ├── _grid.scss │ │ │ ├── _datepicker.scss │ │ │ ├── _timepicker.scss │ │ │ ├── _cards.scss │ │ │ ├── _navbar.scss │ │ │ ├── _sidenav.scss │ │ │ └── _buttons.scss │ │ │ ├── materialize.scss │ │ │ └── fok.scss │ ├── img │ │ ├── logo.png │ │ ├── logo2.png │ │ ├── logo3.png │ │ ├── background.jpg │ │ ├── logo_img.png │ │ ├── background2.jpg │ │ └── logo_img_trans.png │ └── js │ │ ├── component.js │ │ ├── cards.js │ │ ├── characterCounter.js │ │ ├── pushpin.js │ │ └── parallax.js │ ├── apps.py │ ├── templates │ ├── generic.html │ ├── fok │ │ ├── login.html │ │ ├── partials │ │ │ ├── messages.html │ │ │ ├── personal_campaing.html │ │ │ ├── campaign_widget.html │ │ │ └── orcid_button.html │ │ ├── home.html │ │ ├── user_detail.html │ │ ├── campaign_detail.html │ │ └── pledge_detail.html │ ├── widgets │ │ ├── switch_widget.html │ │ └── multiple_checks.html │ └── base.html │ ├── views │ ├── __init__.py │ ├── CampaignView.py │ ├── LoginView.py │ ├── generic.py │ ├── UserProfileView.py │ └── SignPledgeView.py │ ├── context_processors.py │ ├── management │ └── commands │ │ └── generatefakedata.py │ ├── admin.py │ ├── orcid_wrapper.py │ ├── forms.py │ ├── urls.py │ ├── backends.py │ └── models.py ├── free_our_knowledge ├── __init__.py ├── settings │ ├── __init__.py │ └── dev.py ├── wsgi.py ├── urls.py └── resources │ └── fixtures.yml ├── cc_lib ├── __init__.py ├── commands │ ├── __init__.py │ └── generate_fakes_command.py ├── storages.py ├── test_utils.py ├── README.md ├── fixture_helpers.py ├── templates │ └── widgets │ │ └── multiple_checks.html ├── utils.py ├── fixtures.py ├── form_fields.py └── styles │ └── django_forms.scss ├── .gitignore ├── docker ├── entrypoint.sh ├── Dockerfile └── docker-compose.yml ├── TODO ├── Pipfile ├── manage.py ├── requirements.txt ├── LICENSE.md ├── CONTRIBUTING.md └── README.md /apps/cc_cms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/fok/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/fok/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /free_our_knowledge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cc_cms/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc_lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | -------------------------------------------------------------------------------- /apps/cc_cms/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | -------------------------------------------------------------------------------- /apps/fok/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | -------------------------------------------------------------------------------- /apps/fok/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/cc_cms/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "definitions"; 2 | @import "materialize"; 3 | @import "fok"; 4 | -------------------------------------------------------------------------------- /apps/cc_cms/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CmsConfig(AppConfig): 5 | name = 'cc_cms' 6 | -------------------------------------------------------------------------------- /apps/fok/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/logo.png -------------------------------------------------------------------------------- /apps/fok/static/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/logo2.png -------------------------------------------------------------------------------- /apps/fok/static/img/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/logo3.png -------------------------------------------------------------------------------- /free_our_knowledge/settings/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .dev import * 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | env 4 | venv 5 | .idea/ 6 | **.css.map 7 | node_modules 8 | free_our_knowledge/settings/priv/ -------------------------------------------------------------------------------- /apps/fok/static/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/background.jpg -------------------------------------------------------------------------------- /apps/fok/static/img/logo_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/logo_img.png -------------------------------------------------------------------------------- /apps/cc_cms/templates/cc_cms/dynamic_text.html: -------------------------------------------------------------------------------- 1 |
2 | {{ obj.text }} 3 |
-------------------------------------------------------------------------------- /apps/fok/static/img/background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/background2.jpg -------------------------------------------------------------------------------- /apps/fok/static/img/logo_img_trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeOurKnowledge/old_platform/HEAD/apps/fok/static/img/logo_img_trans.png -------------------------------------------------------------------------------- /apps/fok/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FreeOurKnowledgeConfig(AppConfig): 5 | name = 'free_our_knowledge' 6 | -------------------------------------------------------------------------------- /cc_lib/commands/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .generate_fakes_command import GenerateFakesCommand 5 | -------------------------------------------------------------------------------- /apps/fok/templates/generic.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% block generic %} 4 | {% endblock %} 5 | {% endblock %} -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /src/manage.py migrate --noinput 4 | /src/manage.py generatefakedata 5 | /src/manage.py runserver 0.0.0.0:8080 6 | -------------------------------------------------------------------------------- /apps/cc_cms/templates/cc_cms/page.html: -------------------------------------------------------------------------------- 1 | {% extends "generic.html" %} 2 | 3 | {% block generic %} 4 |

{{ object.title }}

5 |
6 | {{ object.text | safe }} 7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Testing database? 2 | Instructions for setting up dev environment (env credentials, docker entrypoint, etc) 3 | Minor: Awkward wrapping of right-hand-side of mailing list switch in initial signup dialog. 4 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/definitions.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Asap|B612'); 2 | 3 | // Materialize 4 | $h1-fontsize: 2.2rem; 5 | 6 | // FOK 7 | $titles-font-family: Asap, Arial, Helvetica, sans-serif; 8 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_icons-material-design.scss: -------------------------------------------------------------------------------- 1 | /* This is needed for some mobile phones to display the Google Icon font properly */ 2 | .material-icons { 3 | text-rendering: optimizeLegibility; 4 | font-feature-settings: 'liga'; 5 | } 6 | -------------------------------------------------------------------------------- /apps/fok/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .SignPledgeView import SignPledgeView 5 | from .UserProfileView import UserProfileView 6 | from .CampaignView import CampaignView 7 | from .LoginView import LoginView 8 | from .generic import NewsletterView 9 | -------------------------------------------------------------------------------- /apps/cc_cms/views.py: -------------------------------------------------------------------------------- 1 | from django.views import generic 2 | from django.apps import apps 3 | 4 | 5 | class PageView(generic.DetailView): 6 | model = apps.get_model('cc_cms', 'Page') 7 | 8 | def get_template_names(self): 9 | return [f'pages/{self.object.slug}.html', 'cc_cms/page.html'] 10 | -------------------------------------------------------------------------------- /apps/cc_cms/resources/cc_cms.scss: -------------------------------------------------------------------------------- 1 | .cms_section { 2 | 3 | } 4 | 5 | .section_columns { 6 | display: flex; 7 | justify-content: space-between; 8 | flex-direction: row; 9 | 10 | div { 11 | text-align: justify; 12 | margin: 5px 20px; 13 | flex-basis: 100%; 14 | } 15 | } -------------------------------------------------------------------------------- /apps/cc_cms/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.urls import path, include 5 | from .views import PageView 6 | 7 | 8 | urlpatterns = [ 9 | path('/', PageView.as_view(), name='page'), 10 | path('summernote/', include('django_summernote.urls')), 11 | ] 12 | -------------------------------------------------------------------------------- /apps/fok/templates/fok/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cc_lib/storages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from storages.backends.s3boto3 import S3Boto3Storage 5 | from django.conf import settings 6 | 7 | 8 | class MediaStorage(S3Boto3Storage): 9 | location = settings.EXTERNAL_MEDIA_PATH 10 | file_overwrite = settings.MEDIA_FILE_OVERWRITE 11 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_transitions.scss: -------------------------------------------------------------------------------- 1 | // Scale transition 2 | .scale-transition { 3 | &.scale-out { 4 | transform: scale(0); 5 | transition: transform .2s !important; 6 | } 7 | 8 | &.scale-in { 9 | transform: scale(1); 10 | } 11 | 12 | transition: transform .3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; 13 | } -------------------------------------------------------------------------------- /apps/fok/templates/fok/partials/messages.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc_lib/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def get_user_fixtures_factory(): 6 | from importlib import import_module 7 | from django.conf import settings 8 | path = settings.USER_FIXTURE_FACTORY_CLASS 9 | s_module, cls = '.'.join(path.split('.')[:-1]), path.split('.')[-1] 10 | module = import_module(s_module) 11 | return getattr(module, cls) 12 | -------------------------------------------------------------------------------- /apps/fok/templates/widgets/switch_widget.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/cc_cms/templates/cc_cms/section.html: -------------------------------------------------------------------------------- 1 |
2 | {% if obj.get_type_display == 'columns' %} 3 | {% for content in obj.content.all %} 4 |
5 | {{ content.text | safe }} 6 |
7 | {% endfor %} 8 | {% elif False %} 9 | {% else %} 10 | Section type do not identified! 11 | {% endif %} 12 |
-------------------------------------------------------------------------------- /apps/cc_cms/admin.py: -------------------------------------------------------------------------------- 1 | from django_summernote.admin import SummernoteModelAdmin 2 | from .models import Page, ColumnsSection, Content, DynamicText 3 | 4 | 5 | class PageAdmin(SummernoteModelAdmin): 6 | summernote_fields = '__all__' 7 | 8 | 9 | def register_cms(admin_site): 10 | admin_site.register(Page, PageAdmin) 11 | admin_site.register(ColumnsSection, PageAdmin) 12 | admin_site.register(Content, PageAdmin) 13 | admin_site.register(DynamicText, PageAdmin) 14 | -------------------------------------------------------------------------------- /apps/fok/migrations/0002_user_is_greeted.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-07-06 17:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('fok', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='is_greeted', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cc_lib/README.md: -------------------------------------------------------------------------------- 1 | # Codi Coop ─ Llibreria d'utilitats 2 | 3 | ## How to... 4 | 5 | ### Use the styles for generated Django forms 6 | 7 | You need to have a SASS compiler. On your static files path (lets imagine that is `apps/your_app/static/styles/scss`) 8 | you will have a main scss file. There you have to include the `cc_lib/styles/django_forms.scss` and 9 | [that's all folks!](https://youtu.be/b9434BoGkNQ) 10 | 11 | ``` 12 | @import "../../../../../cc_lib/styles/django_forms"; 13 | ``` -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | "psycopg2-binary" = "*" 9 | pillow = "*" 10 | django-summernote = "*" 11 | factory-boy = "*" 12 | pyaml = "*" 13 | django-constance = {extras = ["database"], version = "*"} 14 | "boto3" = "*" 15 | django-storages = "*" 16 | django-picklefield = "*" 17 | gunicorn = "*" 18 | orcid = "*" 19 | 20 | [dev-packages] 21 | 22 | [requires] 23 | python_version = "3.6" 24 | -------------------------------------------------------------------------------- /apps/cc_cms/templatetags/section.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .dynamic_text import register 5 | from django.template.loader import render_to_string 6 | from cc_cms.models import Section 7 | 8 | 9 | def section(section_name): 10 | the_section = Section.query(name=section_name) 11 | context = { 12 | 'obj': the_section 13 | } 14 | return render_to_string('cc_cms/section.html', context) 15 | 16 | 17 | register.filter('section', section) 18 | -------------------------------------------------------------------------------- /free_our_knowledge/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for free_our_knowledge project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'free_our_knowledge.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_forms.scss: -------------------------------------------------------------------------------- 1 | // Remove Focus Boxes 2 | select:focus { 3 | outline: $select-focus; 4 | } 5 | 6 | button:focus { 7 | outline: none; 8 | background-color: $button-background-focus; 9 | } 10 | 11 | label { 12 | font-size: $label-font-size; 13 | color: $input-border-color; 14 | } 15 | 16 | @import 'input-fields'; 17 | @import 'radio-buttons'; 18 | @import 'checkboxes'; 19 | @import 'switches'; 20 | @import 'select'; 21 | @import 'file-input'; 22 | @import 'range'; 23 | -------------------------------------------------------------------------------- /apps/cc_cms/templatetags/dynamic_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django import template 5 | from django.template.loader import render_to_string 6 | from cc_cms.models import DynamicText 7 | 8 | 9 | def dynamic_text(text_name): 10 | the_text = DynamicText.objects.get(name=text_name) 11 | context = { 12 | 'obj': the_text 13 | } 14 | return render_to_string('cc_cms/dynamic_text.html', context) 15 | 16 | 17 | register = template.Library() 18 | register.filter('dynamic_text', dynamic_text) 19 | -------------------------------------------------------------------------------- /apps/fok/context_processors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from constance import config 5 | from collections import namedtuple 6 | from django.conf import settings 7 | 8 | OrcidData = namedtuple('OrcidData', 'base_url id redirect_url enabled') 9 | 10 | 11 | def add_config(request): 12 | return { 13 | 'config': config, 14 | 'orcid': OrcidData( 15 | base_url=settings.BASE_ORCID_URL, 16 | id=settings.ORCID_ID, 17 | redirect_url=settings.ORCID_REDIRECT_URL, 18 | enabled=settings.USE_ORCID 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/fok/views/CampaignView.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.views.generic import DetailView 5 | from django.apps import apps 6 | 7 | 8 | class CampaignView(DetailView): 9 | http_method_names = ['get'] 10 | model = apps.get_model('fok', 'Campaign') 11 | 12 | def get_context_data(self, **kwargs): 13 | from ..models import Pledge 14 | pledge = Pledge.objects.filter(user=self.request.user, campaign=self.object).first() \ 15 | if not self.request.user.is_anonymous else None 16 | return super().get_context_data(pledge=pledge) 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'free_our_knowledge.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | 3 | WORKDIR /srv 4 | 5 | RUN pip install --upgrade pip 6 | RUN apk update 7 | RUN apk add build-base python3-dev py-pip jpeg-dev zlib-dev 8 | RUN apk add --virtual build-deps gcc python3-dev musl-dev 9 | RUN apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev py-pillow 10 | RUN apk add postgresql-dev 11 | RUN apk add libffi-dev 12 | RUN apk add libxslt-dev 13 | 14 | ADD requirements.txt /srv/requirements.txt 15 | RUN pip install -r /srv/requirements.txt 16 | 17 | WORKDIR /src 18 | 19 | ADD docker/entrypoint.sh /src/entrypoint.sh 20 | COPY . /src 21 | 22 | ENTRYPOINT ["/bin/sh", "/src/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /apps/cc_cms/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from cc_lib.fixtures import DjangoFactory 5 | from django.apps import apps 6 | 7 | 8 | class ContentFactory(DjangoFactory): 9 | class Meta: 10 | model = apps.get_model('cc_cms', 'Content') 11 | 12 | 13 | class DynamicTextFactory(DjangoFactory): 14 | class Meta: 15 | model = apps.get_model('cc_cms', 'DynamicText') 16 | 17 | 18 | class ColumnsFactory(DjangoFactory): 19 | class Meta: 20 | model = apps.get_model('cc_cms', 'ColumnsSection') 21 | 22 | 23 | class PageFactory(DjangoFactory): 24 | class Meta: 25 | model = apps.get_model('cc_cms', 'Page') 26 | 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.7.1 2 | boto3==1.9.120 3 | botocore==1.12.120 4 | certifi==2019.3.9 5 | chardet==3.0.4 6 | Django==2.1.7 7 | django-constance==2.4.0 8 | django-picklefield==2.0 9 | django-storages==1.7.1 10 | django-summernote==0.8.11.4 11 | docutils==0.14 12 | factory-boy==2.11.1 13 | Faker==1.0.4 14 | gunicorn==19.9.0 15 | html5lib==1.0.1 16 | idna==2.8 17 | jmespath==0.9.4 18 | lxml==4.3.2 19 | orcid==1.0.3 20 | Pillow==5.4.1 21 | psycopg2-binary==2.7.7 22 | pyaml==18.11.0 23 | python-dateutil==2.8.0 24 | pytz==2018.9 25 | PyYAML==5.1 26 | requests==2.21.0 27 | s3transfer==0.2.0 28 | simplejson==3.16.0 29 | six==1.12.0 30 | soupsieve==1.8 31 | text-unidecode==1.2 32 | urllib3==1.24.1 33 | webencodings==0.5.1 34 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | fok_app: 5 | image: fok_platform 6 | # build: ./ 7 | restart: always 8 | networks: 9 | - mynetwork 10 | expose: 11 | - "8080" 12 | ports: 13 | - "8080:8080" 14 | depends_on: 15 | - db 16 | volumes: 17 | - ../apps:/src/apps 18 | - ../free_our_knowledge:/src/free_our_knowledge 19 | 20 | db: 21 | image: postgres 22 | restart: always 23 | networks: 24 | - mynetwork 25 | environment: 26 | POSTGRES_USER: "postgres" 27 | POSTGRES_PASSWORD: "mysecretpassword" 28 | POSTGRES_DB: "fok" 29 | expose: 30 | - "5432" 31 | ports: 32 | - "5432:5432" 33 | 34 | networks: {mynetwork: {}} 35 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_pulse.scss: -------------------------------------------------------------------------------- 1 | .pulse { 2 | &::before { 3 | content: ''; 4 | display: block; 5 | position: absolute; 6 | width: 100%; 7 | height: 100%; 8 | top: 0; 9 | left: 0; 10 | background-color: inherit; 11 | border-radius: inherit; 12 | transition: opacity .3s, transform .3s; 13 | animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; 14 | z-index: -1; 15 | } 16 | 17 | overflow: visible; 18 | position: relative; 19 | } 20 | 21 | @keyframes pulse-animation { 22 | 0% { 23 | opacity: 1; 24 | transform: scale(1); 25 | } 26 | 50% { 27 | opacity: 0; 28 | transform: scale(1.5); 29 | } 30 | 100% { 31 | opacity: 0; 32 | transform: scale(1.5); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .material-tooltip { 2 | padding: 10px 8px; 3 | font-size: 1rem; 4 | z-index: 2000; 5 | background-color: transparent; 6 | border-radius: 2px; 7 | color: #fff; 8 | min-height: 36px; 9 | line-height: 120%; 10 | opacity: 0; 11 | position: absolute; 12 | text-align: center; 13 | max-width: calc(100% - 4px); 14 | overflow: hidden; 15 | left: 0; 16 | top: 0; 17 | pointer-events: none; 18 | visibility: hidden; 19 | background-color: #323232; 20 | } 21 | 22 | .backdrop { 23 | position: absolute; 24 | opacity: 0; 25 | height: 7px; 26 | width: 14px; 27 | border-radius: 0 0 50% 50%; 28 | background-color: #323232; 29 | z-index: -1; 30 | transform-origin: 50% 0%; 31 | visibility: hidden; 32 | } 33 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_table_of_contents.scss: -------------------------------------------------------------------------------- 1 | /*************** 2 | Nav List 3 | ***************/ 4 | .table-of-contents { 5 | &.fixed { 6 | position: fixed; 7 | } 8 | 9 | li { 10 | padding: 2px 0; 11 | } 12 | a { 13 | display: inline-block; 14 | font-weight: 300; 15 | color: #757575; 16 | padding-left: 16px; 17 | height: 1.5rem; 18 | line-height: 1.5rem; 19 | letter-spacing: .4; 20 | display: inline-block; 21 | 22 | &:hover { 23 | color: lighten(#757575, 20%); 24 | padding-left: 15px; 25 | border-left: 1px solid $primary-color; 26 | } 27 | &.active { 28 | font-weight: 500; 29 | padding-left: 14px; 30 | border-left: 2px solid $primary-color; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/fok/views/LoginView.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.contrib.auth import authenticate, login as auth_login 5 | from django.views.generic import TemplateView 6 | from django.views.decorators.cache import never_cache 7 | from django.utils.decorators import method_decorator 8 | from django.conf import settings 9 | from django.shortcuts import redirect 10 | 11 | 12 | class LoginView(TemplateView): 13 | template_name = 'fok/login.html' 14 | 15 | @method_decorator(never_cache) 16 | def dispatch(self, request, *args, **kwargs): 17 | user = authenticate(request) 18 | auth_login(request, user=user) 19 | if not settings.USE_ORCID: 20 | return redirect('home') 21 | return super().dispatch(request, *args, **kwargs) 22 | -------------------------------------------------------------------------------- /apps/fok/migrations/0003_auto_20190707_0417.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-07-07 04:17 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('fok', '0002_user_is_greeted'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='background', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='fok.Background', verbose_name='Research Field'), 18 | ), 19 | migrations.AlterField( 20 | model_name='user', 21 | name='password', 22 | field=models.CharField(blank=True, max_length=20, null=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /apps/fok/templates/fok/partials/personal_campaing.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
{{ campaign.title }}
6 | {% if campaign.short_description %}{{ campaign.short_description }}{% else %}{{ campaign.description|truncatechars:200 }}{% endif %} 7 |
8 |
9 | 14 |
-------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_materialbox.scss: -------------------------------------------------------------------------------- 1 | .materialboxed { 2 | &:hover { 3 | &:not(.active) { 4 | opacity: .8; 5 | } 6 | } 7 | 8 | display: block; 9 | cursor: zoom-in; 10 | position: relative; 11 | transition: opacity .4s; 12 | -webkit-backface-visibility: hidden; 13 | 14 | &.active { 15 | cursor: zoom-out; 16 | } 17 | } 18 | 19 | #materialbox-overlay { 20 | position:fixed; 21 | top: 0; 22 | right: 0; 23 | bottom: 0; 24 | left: 0; 25 | background-color: #292929; 26 | z-index: 1000; 27 | will-change: opacity; 28 | } 29 | 30 | .materialbox-caption { 31 | position: fixed; 32 | display: none; 33 | color: #fff; 34 | line-height: 50px; 35 | bottom: 0; 36 | left: 0; 37 | width: 100%; 38 | text-align: center; 39 | padding: 0% 15%; 40 | height: 50px; 41 | z-index: 1000; 42 | -webkit-font-smoothing: antialiased; 43 | } -------------------------------------------------------------------------------- /free_our_knowledge/urls.py: -------------------------------------------------------------------------------- 1 | """free_our_knowledge URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.urls import path, include 18 | 19 | 20 | urlpatterns = [ 21 | path('', include('fok.urls')), 22 | path('pages/', include('cc_cms.urls')) 23 | ] 24 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_color-classes.scss: -------------------------------------------------------------------------------- 1 | // Color Classes 2 | 3 | @each $color_name, $color in $colors { 4 | @each $color_type, $color_value in $color { 5 | @if $color_type == "base" { 6 | .#{$color_name} { 7 | background-color: $color_value !important; 8 | } 9 | .#{$color_name}-text { 10 | color: $color_value !important; 11 | } 12 | } 13 | @else if $color_name != "shades" { 14 | .#{$color_name}.#{$color_type} { 15 | background-color: $color_value !important; 16 | } 17 | .#{$color_name}-text.text-#{$color_type} { 18 | color: $color_value !important; 19 | } 20 | } 21 | } 22 | } 23 | 24 | // Shade classes 25 | @each $color, $color_value in $shades { 26 | .#{$color} { 27 | background-color: $color_value !important; 28 | } 29 | .#{$color}-text { 30 | color: $color_value !important; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/fok/views/generic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.views import View 5 | from ..forms import NewsletterForm 6 | from django.shortcuts import redirect 7 | from django.contrib import messages 8 | 9 | 10 | class NewsletterView(View): 11 | def post(self, request, *args, **kwargs): 12 | values = request.POST.copy() 13 | if 'newsletter' in values.keys() and not values['email']: 14 | messages.error(request, 'You accepted to receive the newsletter, however your email address was not given.') 15 | return redirect(request.META['HTTP_REFERER']) 16 | redirect_url = values.pop('redirect_url')[0] 17 | form = NewsletterForm(instance=request.user, data=values) 18 | if form.is_valid(): 19 | user = form.save() 20 | user.is_greeted = True 21 | user.save() 22 | return redirect(redirect_url) 23 | -------------------------------------------------------------------------------- /apps/fok/management/commands/generatefakedata.py: -------------------------------------------------------------------------------- 1 | from cc_lib.commands import GenerateFakesCommand # as Command 2 | import random 3 | 4 | 5 | class Command(GenerateFakesCommand): 6 | def fakes_generation_finished(self): 7 | pledges = self.get_generated_objects('Pledge') 8 | user_campaign = [] 9 | positions = self.get_generated_objects('EnabledAuthorPosition') 10 | 11 | for pledge in pledges: 12 | if (pledge.campaign.pk, pledge.user.pk) in user_campaign: 13 | pledge.delete() 14 | else: 15 | user_campaign.append((pledge.campaign.pk, pledge.user.pk)) 16 | n_positions = random.randint(1, len(positions)) 17 | taken_positions = random.sample(positions, n_positions) 18 | for position in taken_positions: 19 | pledge.author_position.add(position) 20 | pledge.save() 21 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_file-input.scss: -------------------------------------------------------------------------------- 1 | /* File Input 2 | ========================================================================== */ 3 | 4 | .file-field { 5 | position: relative; 6 | 7 | .file-path-wrapper { 8 | overflow: hidden; 9 | padding-left: 10px; 10 | } 11 | 12 | input.file-path { width: 100%; } 13 | 14 | .btn { 15 | float: left; 16 | height: $input-height; 17 | line-height: $input-height; 18 | } 19 | 20 | span { 21 | cursor: pointer; 22 | } 23 | 24 | input[type=file] { 25 | 26 | // Needed to override webkit button 27 | &::-webkit-file-upload-button { 28 | display: none; 29 | } 30 | 31 | position: absolute; 32 | top: 0; 33 | right: 0; 34 | left: 0; 35 | bottom: 0; 36 | width: 100%; 37 | margin: 0; 38 | padding: 0; 39 | font-size: 20px; 40 | cursor: pointer; 41 | opacity: 0; 42 | filter: alpha(opacity=0); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/fok/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import User, Background, Campaign, Pledge 3 | from cc_cms.admin import register_cms, Page, PageAdmin 4 | from constance.admin import ConstanceAdmin, Config 5 | from django_summernote.admin import SummernoteModelAdmin 6 | from django.contrib.auth.decorators import login_required 7 | 8 | 9 | class FokAdmin(admin.AdminSite): 10 | site_header = "Free Our Knowledge" 11 | site_title = "FOK Admin" 12 | 13 | 14 | class FokPageAdmin(PageAdmin): 15 | exclude = ('intro',) 16 | 17 | 18 | class CampaignAdmin(SummernoteModelAdmin): 19 | summernote_fields = '__all__' 20 | 21 | 22 | fok_admin_site = FokAdmin(name='fok_admin') 23 | fok_admin_site.register(User) 24 | fok_admin_site.register(Background) 25 | fok_admin_site.register(Campaign, CampaignAdmin) 26 | fok_admin_site.register(Pledge) 27 | fok_admin_site.register([Config], ConstanceAdmin) 28 | 29 | register_cms(fok_admin_site) 30 | 31 | fok_admin_site.unregister(Page) 32 | fok_admin_site.register(Page, FokPageAdmin) 33 | fok_admin_site.login = login_required(admin.site.login) 34 | -------------------------------------------------------------------------------- /cc_lib/fixture_helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from factory import fuzzy 5 | from django.utils import timezone 6 | import datetime 7 | 8 | 9 | def one_moment_in_the_last_days(days): 10 | return fuzzy.FuzzyDateTime( 11 | start_dt=timezone.now() - datetime.timedelta(days=days), 12 | end_dt=timezone.now() 13 | ) 14 | 15 | 16 | def one_past_moment_between_days(start_days, end_days): 17 | return fuzzy.FuzzyDateTime( 18 | start_dt=timezone.now() - datetime.timedelta(days=start_days), 19 | end_dt=timezone.now() - datetime.timedelta(days=end_days) 20 | ) 21 | 22 | 23 | def one_moment_in_the_next_days(days): 24 | return fuzzy.FuzzyDateTime( 25 | start_dt=timezone.now(), 26 | end_dt=timezone.now() + datetime.timedelta(days=days) 27 | ) 28 | 29 | 30 | def one_future_moment_between_days(start_days, end_days): 31 | return fuzzy.FuzzyDateTime( 32 | start_dt=timezone.now() + datetime.timedelta(days=start_days), 33 | end_dt=timezone.now() + datetime.timedelta(days=end_days) 34 | ) 35 | -------------------------------------------------------------------------------- /apps/fok/orcid_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | from django.conf import settings 6 | 7 | # https://members.orcid.org/api/tutorial/read-orcid-records 8 | 9 | 10 | def authorize(auth_code): 11 | data = requests.post( 12 | f'{settings.BASE_ORCID_URL}/oauth/token', 13 | data={ 14 | 'client_id': settings.ORCID_ID, 15 | 'client_secret': settings.ORCID_SECRET, 16 | 'grant_type': 'authorization_code', 17 | 'code': auth_code, 18 | 'redirect_uri': settings.ORCID_REDIRECT_URL 19 | }, 20 | headers={ 21 | 'accept': 'application/json' 22 | } 23 | ) 24 | assert data.status_code == 200, str(data.content) 25 | return data.json() 26 | 27 | 28 | def obtain_data(user_id, token): 29 | data = requests.get( 30 | f'{settings.BASE_ORCID_API_URL}/{user_id}/email', 31 | headers={ 32 | 'accept': 'application/json', 33 | 'Authorization': f'Bearer {token}' 34 | } 35 | ) 36 | return data.json() 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Free Our Knowledge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_toast.scss: -------------------------------------------------------------------------------- 1 | #toast-container { 2 | display:block; 3 | position: fixed; 4 | z-index: 10000; 5 | 6 | @media #{$small-and-down} { 7 | min-width: 100%; 8 | bottom: 0%; 9 | } 10 | @media #{$medium-only} { 11 | left: 5%; 12 | bottom: 7%; 13 | max-width: 90%; 14 | } 15 | @media #{$large-and-up} { 16 | top: 10%; 17 | right: 7%; 18 | max-width: 86%; 19 | } 20 | } 21 | 22 | .toast { 23 | @extend .z-depth-1; 24 | border-radius: 2px; 25 | top: 35px; 26 | width: auto; 27 | margin-top: 10px; 28 | position: relative; 29 | max-width:100%; 30 | height: auto; 31 | min-height: $toast-height; 32 | line-height: 1.5em; 33 | background-color: $toast-color; 34 | padding: 10px 25px; 35 | font-size: 1.1rem; 36 | font-weight: 300; 37 | color: $toast-text-color; 38 | display: flex; 39 | align-items: center; 40 | justify-content: space-between; 41 | cursor: default; 42 | 43 | .toast-action { 44 | color: $toast-action-color; 45 | font-weight: 500; 46 | margin-right: -25px; 47 | margin-left: 3rem; 48 | } 49 | 50 | &.rounded{ 51 | border-radius: 24px; 52 | } 53 | 54 | @media #{$small-and-down} { 55 | width: 100%; 56 | border-radius: 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/materialize.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // Color 4 | @import "components/color-variables"; 5 | @import "components/color-classes"; 6 | 7 | // Variables; 8 | @import "components/variables"; 9 | 10 | // Reset 11 | @import "components/normalize"; 12 | 13 | // components 14 | @import "components/global"; 15 | @import "components/badges"; 16 | @import "components/icons-material-design"; 17 | @import "components/grid"; 18 | @import "components/navbar"; 19 | @import "components/typography"; 20 | @import "components/transitions"; 21 | @import "components/cards"; 22 | @import "components/toast"; 23 | @import "components/tabs"; 24 | @import "components/tooltip"; 25 | @import "components/buttons"; 26 | @import "components/dropdown"; 27 | @import "components/waves"; 28 | @import "components/modal"; 29 | @import "components/collapsible"; 30 | @import "components/chips"; 31 | @import "components/materialbox"; 32 | @import "components/forms/forms"; 33 | @import "components/table_of_contents"; 34 | @import "components/sidenav"; 35 | @import "components/preloader"; 36 | @import "components/slider"; 37 | @import "components/carousel"; 38 | @import "components/tapTarget"; 39 | @import "components/pulse"; 40 | @import "components/datepicker"; 41 | @import "components/timepicker"; 42 | -------------------------------------------------------------------------------- /apps/fok/static/js/component.js: -------------------------------------------------------------------------------- 1 | class Component { 2 | /** 3 | * Generic constructor for all components 4 | * @constructor 5 | * @param {Element} el 6 | * @param {Object} options 7 | */ 8 | constructor(classDef, el, options) { 9 | // Display error if el is valid HTML Element 10 | if (!(el instanceof Element)) { 11 | console.error(Error(el + ' is not an HTML Element')); 12 | } 13 | 14 | // If exists, destroy and reinitialize in child 15 | let ins = classDef.getInstance(el); 16 | if (!!ins) { 17 | ins.destroy(); 18 | } 19 | 20 | this.el = el; 21 | this.$el = cash(el); 22 | } 23 | 24 | /** 25 | * Initializes components 26 | * @param {class} classDef 27 | * @param {Element | NodeList | jQuery} els 28 | * @param {Object} options 29 | */ 30 | static init(classDef, els, options) { 31 | let instances = null; 32 | if (els instanceof Element) { 33 | instances = new classDef(els, options); 34 | } else if (!!els && (els.jquery || els.cash || els instanceof NodeList)) { 35 | let instancesArr = []; 36 | for (let i = 0; i < els.length; i++) { 37 | instancesArr.push(new classDef(els[i], options)); 38 | } 39 | instances = instancesArr; 40 | } 41 | 42 | return instances; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_badges.scss: -------------------------------------------------------------------------------- 1 | // Badges 2 | span.badge { 3 | min-width: 3rem; 4 | padding: 0 6px; 5 | margin-left: 14px; 6 | text-align: center; 7 | font-size: 1rem; 8 | line-height: $badge-height; 9 | height: $badge-height; 10 | color: color('grey', 'darken-1'); 11 | float: right; 12 | box-sizing: border-box; 13 | 14 | &.new { 15 | font-weight: 300; 16 | font-size: 0.8rem; 17 | color: #fff; 18 | background-color: $badge-bg-color; 19 | border-radius: 2px; 20 | } 21 | &.new:after { 22 | content: " new"; 23 | } 24 | 25 | &[data-badge-caption]::after { 26 | content: " " attr(data-badge-caption); 27 | } 28 | } 29 | 30 | // Special cases 31 | nav ul a span.badge { 32 | display: inline-block; 33 | float: none; 34 | margin-left: 4px; 35 | line-height: $badge-height; 36 | height: $badge-height; 37 | -webkit-font-smoothing: auto; 38 | } 39 | 40 | // Line height centering 41 | .collection-item span.badge { 42 | margin-top: calc(#{$collection-line-height / 2} - #{$badge-height / 2}); 43 | } 44 | .collapsible span.badge { 45 | margin-left: auto; 46 | } 47 | .sidenav span.badge { 48 | margin-top: calc(#{$sidenav-line-height / 2} - #{$badge-height / 2}); 49 | } 50 | 51 | table span.badge { 52 | display: inline-block; 53 | float: none; 54 | margin-left: auto; 55 | } 56 | -------------------------------------------------------------------------------- /apps/fok/templates/fok/partials/campaign_widget.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
{{ campaign.title }}
6 | {% if campaign.short_description %}{{ campaign.short_description }}{% else %}{{ campaign.description|truncatechars:200 }}{% endif %} 7 |
8 | {% if view.request.user.is_anonymous %} 9 |
10 | Login & pledge 11 |
12 | {% elif campaign in view.request.user.pledged_campaigns %} 13 |
14 | Thank you for your pledge 15 |
16 | {% else %} 17 |
18 | Pledge 19 |
20 | {% endif %} 21 |
22 |
-------------------------------------------------------------------------------- /apps/fok/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .models import Pledge, User, EnabledAuthorPosition 5 | from django import forms 6 | from cc_lib.form_fields import SelectMultipleChecksField, SwitchWidget 7 | 8 | 9 | class SignPledgeForm(forms.ModelForm): 10 | class Meta: 11 | model = Pledge 12 | fields = ['implication', 'author_position', 'allow_public_name'] 13 | implication = forms.DecimalField(max_value=100, min_value=0, label='Threshold') 14 | author_position = SelectMultipleChecksField() 15 | allow_public_name = forms.BooleanField(required=False) 16 | 17 | def __init__(self, *args, **kwargs): 18 | self.base_fields['author_position'].choices = EnabledAuthorPosition.objects.all() 19 | self.base_fields['author_position'].defaults = [EnabledAuthorPosition.objects.all().first()] 20 | super().__init__(*args, **kwargs) 21 | 22 | 23 | class UserDataForm(forms.ModelForm): 24 | class Meta: 25 | model = User 26 | fields = ['name', 'email', 'newsletter'] 27 | widgets = { 28 | 'newsletter': SwitchWidget 29 | } 30 | 31 | 32 | class NewsletterForm(forms.ModelForm): 33 | class Meta: 34 | model = User 35 | fields = ['email', 'newsletter'] 36 | widgets = { 37 | 'newsletter': SwitchWidget 38 | } 39 | -------------------------------------------------------------------------------- /apps/fok/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.urls import path 5 | from .admin import fok_admin_site 6 | from django.views.generic import TemplateView 7 | 8 | from .models import Campaign 9 | from .views import SignPledgeView, UserProfileView, CampaignView, LoginView, NewsletterView 10 | from django.contrib.auth.views import LogoutView 11 | 12 | 13 | urlpatterns = [ 14 | path('', TemplateView.as_view( 15 | template_name="fok/home.html", 16 | extra_context={ 17 | # The lambda makes this expression to be executed each call of home (because of the admin updates) 18 | 'campaigns': lambda: Campaign.objects.filter(visible=True).order_by('position', '-created_at') 19 | } 20 | ), name='home'), 21 | path('admin/', fok_admin_site.urls, name='admin'), 22 | path('campaign/', CampaignView.as_view(), name='campaign'), 23 | path('user/', UserProfileView.as_view(), name='user'), 24 | path('pledge/', SignPledgeView.as_view(), name='pledge'), 25 | path('pledge//sign/', SignPledgeView.as_view(), name='sign'), 26 | path('login/', LoginView.as_view(), name='login'), 27 | path('logout', LogoutView.as_view( 28 | next_page='home' 29 | ), name='logout'), 30 | path('newsletter/', NewsletterView.as_view(), name='newsletter') 31 | ] 32 | -------------------------------------------------------------------------------- /cc_lib/templates/widgets/multiple_checks.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% for choice in choices %} 4 |
{{ choice }}
10 | {% endfor %} 11 | 30 |
-------------------------------------------------------------------------------- /apps/fok/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.contrib.auth import get_user_model 5 | from cc_lib.fixtures import DjangoFactory 6 | from ..models import Background, Campaign, Pledge, EnabledAuthorPosition 7 | import factory 8 | from cc_lib.utils import storage_files 9 | from django.conf import settings 10 | 11 | 12 | class UserFactory(DjangoFactory): 13 | class Meta: 14 | model = get_user_model() 15 | 16 | 17 | class BackgroundFactory(DjangoFactory): 18 | class Meta: 19 | model = Background 20 | name = factory.Iterator(['Mathematics', 'Biology', 'Neuroscience', 'Physics', 'Chemistry', 'Astronomy']) 21 | 22 | 23 | class CampaignFactory(DjangoFactory): 24 | class Meta: 25 | model = Campaign 26 | visible = True 27 | criteria = factory.Faker('text', max_nb_chars=2000) 28 | image = 'https://www.emacswiki.org/pics/static/KitchenSinkBW.png' 29 | # factory.fuzzy.FuzzyChoice( 30 | # storage_files( 31 | # settings.FIXTURES_PATH_TO_COVER_IMAGES, 32 | # f'http://{settings.AWS_S3_CUSTOM_DOMAIN}/{settings.AWS_STORAGE_BUCKET_NAME}' 33 | # ) 34 | #) 35 | 36 | 37 | class EnabledAuthorPositionFactory(DjangoFactory): 38 | class Meta: 39 | model = EnabledAuthorPosition 40 | 41 | 42 | class PledgeFactory(DjangoFactory): 43 | class Meta: 44 | model = Pledge 45 | -------------------------------------------------------------------------------- /apps/fok/templates/widgets/multiple_checks.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% for choice in choices %} 4 |
5 | 10 |
11 | {% endfor %} 12 | 31 |
-------------------------------------------------------------------------------- /apps/fok/views/UserProfileView.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.views.generic import TemplateView 5 | from django.contrib.auth.forms import PasswordChangeForm 6 | from ..forms import UserDataForm, SignPledgeForm 7 | from ..models import Pledge 8 | from django.contrib import messages 9 | 10 | 11 | class UserProfileView(TemplateView): 12 | template_name = "fok/user_detail.html" 13 | 14 | def get_context_data(self, **kwargs): 15 | return super().get_context_data( 16 | password_change_form=PasswordChangeForm(self.request.user), 17 | user_data_form=UserDataForm(instance=self.request.user) 18 | ) 19 | 20 | def post(self, request, *args, **kwargs): 21 | _type = request.POST['type'] 22 | if _type == 'password': 23 | form = PasswordChangeForm(self.request.user, self.request.POST) 24 | successful_message = 'Password successfully updated' 25 | elif _type == 'user_data': 26 | form = UserDataForm(instance=self.request.user, data=self.request.POST) 27 | successful_message = 'User data successfully updated' 28 | 29 | if form.is_valid(): 30 | form.save() 31 | messages.success(request, successful_message) 32 | else: 33 | for value in form.errors.values(): 34 | messages.error(request, value.as_text().replace('* ', '')) 35 | return self.get(request, *args, **kwargs) 36 | -------------------------------------------------------------------------------- /apps/fok/static/js/cards.js: -------------------------------------------------------------------------------- 1 | (function($, anim) { 2 | $(document).on('click', '.card', function(e) { 3 | if ($(this).children('.card-reveal').length) { 4 | var $card = $(e.target).closest('.card'); 5 | if ($card.data('initialOverflow') === undefined) { 6 | $card.data( 7 | 'initialOverflow', 8 | $card.css('overflow') === undefined ? '' : $card.css('overflow') 9 | ); 10 | } 11 | let $cardReveal = $(this).find('.card-reveal'); 12 | if ( 13 | $(e.target).is($('.card-reveal .card-title')) || 14 | $(e.target).is($('.card-reveal .card-title i')) 15 | ) { 16 | // Make Reveal animate down and display none 17 | anim({ 18 | targets: $cardReveal[0], 19 | translateY: 0, 20 | duration: 225, 21 | easing: 'easeInOutQuad', 22 | complete: function(anim) { 23 | let el = anim.animatables[0].target; 24 | $(el).css({ display: 'none' }); 25 | $card.css('overflow', $card.data('initialOverflow')); 26 | } 27 | }); 28 | } else if ($(e.target).is($('.card .activator')) || $(e.target).is($('.card .activator i'))) { 29 | $card.css('overflow', 'hidden'); 30 | $cardReveal.css({ display: 'block' }); 31 | anim({ 32 | targets: $cardReveal[0], 33 | translateY: '-100%', 34 | duration: 300, 35 | easing: 'easeInOutQuad' 36 | }); 37 | } 38 | } 39 | }); 40 | })(cash, M.anime); 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Project structure 4 | 5 | ``` 6 | platform 7 | ├── free_our_knowledge 8 | │ ├── __init__.py 9 | │ ├── settings 10 | │ ├── resources 11 | │ ├── urls.py 12 | │ └── wsgi.py 13 | ├── docker 14 | │ └── Dockerfile 15 | │ └── docker-compose.yml 16 | │ └── entrypoint.sh 17 | ├── manage.py 18 | └── requirements.txt 19 | ``` 20 | 21 | ## Getting Started 22 | 23 | 24 | ### Development installation 25 | 26 | 1. CLone the repository locally: 27 | 28 | ``` 29 | git clone git@github.com:FreeOurKnowledge/platform.git 30 | ``` 31 | 32 | 2. Create a new virtual environment and activate it: 33 | 34 | ``` 35 | python3 -m venv fok_env 36 | source fok_env/bin/activate 37 | ``` 38 | 39 | 3. Install all of the dependencies: 40 | 41 | ``` 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | 4. Generate the database tables: 46 | 47 | ``` 48 | python manage.py migrate --noinput 49 | ``` 50 | 51 | 4. Start the project: 52 | 53 | ```python 54 | python manage.py runserver 55 | ``` 56 | 57 | If everything works the platform should be available on http://127.0.0.1:8000/. 58 | 59 | ### Docker installation 60 | 61 | 1. Build the development image. From the project root: 62 | 63 | sudo docker build . -f ./docker/Dockerfile -t fok_platform 64 | 65 | sudo docker-compose -f ./docker/docker-compose.yml up --build -d 66 | 67 | 2. Check the build is running properly 68 | 69 | sudo docker ps 70 | 71 | 3. [Configure credentials]?? 72 | 73 | 4. Open a browser to http://localhost:8080/ to view the application (if using Chromebook, go to http://penguin.termina.linux.test:8080/) 74 | 75 | 76 | -------------------------------------------------------------------------------- /apps/fok/backends.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .models import User 5 | from .orcid_wrapper import authorize, obtain_data 6 | from django.conf import settings 7 | import string 8 | import random 9 | import factory 10 | 11 | 12 | class OrcidBackend: 13 | def auth_orcid(self, request): 14 | code = request.GET['code'] 15 | data = authorize(code) 16 | user, created = User.objects.get_or_create(username=data['orcid']) 17 | if created: 18 | user.first_name = data['name'] 19 | user.name = data['name'] 20 | new_data = obtain_data(data['orcid'], data['access_token']) 21 | user.save() 22 | return user 23 | 24 | def auth_debug(self, request): 25 | orcid = ''.join(random.choice(string.ascii_lowercase) for _ in range(10)) 26 | user, created = User.objects.get_or_create( 27 | username=orcid, 28 | defaults={ 29 | 'is_staff': True, 30 | 'is_superuser': True 31 | } 32 | ) 33 | if created: 34 | user.first_name = factory.Faker('first_name').generate([]) 35 | user.name = factory.Faker('first_name').generate([]) 36 | user.save() 37 | return user 38 | 39 | def authenticate(self, request, *args, **kwargs): 40 | return self.auth_orcid(request) if settings.USE_ORCID else self.auth_debug(request) 41 | 42 | def get_user(self, user_id): 43 | try: 44 | return User.objects.get(pk=user_id) 45 | except User.DoesNotExist: 46 | return None 47 | -------------------------------------------------------------------------------- /free_our_knowledge/resources/fixtures.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fok.tests.fixtures.BackgroundFactory: 3 | number: 6 4 | 5 | fok.tests.fixtures.UserFactory: 6 | number: 25 7 | related: 8 | background: 9 | strategy: iterate 10 | extra_objects: 11 | - number: 1 12 | fields_value: 13 | email: admin@codi.coop 14 | password: test 15 | is_staff: true 16 | is_superuser: true 17 | 18 | cc_cms.tests.fixtures.ContentFactory: 19 | number: 8 20 | 21 | cc_cms.tests.fixtures.ColumnsFactory: 22 | number: 3 23 | field_values: 24 | name: 25 | - introduction 26 | - campaigns 27 | - footer 28 | related: 29 | content: 30 | strategy: sample 31 | number: 1 32 | 33 | cc_cms.tests.fixtures.PageFactory: 34 | number: 4 35 | field_values: 36 | name: 37 | - faq 38 | - news 39 | - contribute 40 | - about 41 | 42 | cc_cms.tests.fixtures.DynamicTextFactory: 43 | number: 3 44 | field_values: 45 | name: 46 | - pledge1 47 | - pledge2 48 | - pledge3 49 | 50 | fok.tests.fixtures.CampaignFactory: 51 | number: 5 52 | field_values: 53 | position: 54 | - 0 55 | - 10 56 | - 20 57 | - 30 58 | - 40 59 | 60 | fok.tests.fixtures.EnabledAuthorPositionFactory: 61 | number: 3 62 | field_values: 63 | position: 64 | - First 65 | - Middle 66 | - Last 67 | 68 | fok.tests.fixtures.PledgeFactory: 69 | number: 100 70 | related: 71 | user: 72 | strategy: choice 73 | campaign: 74 | strategy: choice 75 | field_values: 76 | allow_public_name: 77 | - true 78 | - false 79 | implication: 80 | - 10 81 | - 20 82 | - 40 83 | - 50 84 | - 80 85 | - 90 -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/fok.scss: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: "B612"; 3 | } 4 | 5 | h1,h2,h3,h4,h5 { 6 | font-family: $titles-font-family; 7 | } 8 | 9 | main { 10 | min-height: 100vh; 11 | } 12 | 13 | header { 14 | height: 80px; 15 | 16 | img { 17 | zoom: 0.7; 18 | } 19 | 20 | .menu { 21 | display: flex; 22 | flex-wrap: wrap; 23 | justify-content: space-between; 24 | 25 | a { 26 | font-family: $titles-font-family; 27 | color: black; 28 | font-size: 1.3rem; 29 | color: #f68212; 30 | } 31 | } 32 | } 33 | 34 | section { 35 | margin: 20px 0; 36 | } 37 | 38 | hr { 39 | width: 25%; 40 | margin-top: 50px; 41 | margin-bottom: 50px; 42 | border-color: ligtgray; 43 | } 44 | 45 | footer { 46 | color: black; 47 | margin-top: 50px; 48 | } 49 | 50 | .frontpage { 51 | min-height: 590px; 52 | position: relative; 53 | 54 | h4 { 55 | background-color: rgba(255, 255, 255, 0.2); 56 | padding: 20px; 57 | -webkit-user-select: none; 58 | -moz-user-select: none; 59 | -ms-user-select: none; 60 | user-select: none; 61 | } 62 | } 63 | 64 | .card-panel { 65 | padding: 0; 66 | position: relative; 67 | overflow:hidden; 68 | background-color: transparent; 69 | 70 | .pledge { 71 | padding: 15px; 72 | background-color: transparent; 73 | margin-bottom: 0; 74 | } 75 | 76 | .img-cover { 77 | max-width: 100%; 78 | max-height: 100%; 79 | } 80 | 81 | .img-background { 82 | position: absolute; 83 | width: 400px; 84 | height: 400px; 85 | bottom: -130px; 86 | left: -130px; 87 | opacity: 0.2; 88 | z-index: -1; 89 | } 90 | } -------------------------------------------------------------------------------- /apps/fok/templates/fok/partials/orcid_button.html: -------------------------------------------------------------------------------- 1 | 28 | {% if orcid.enabled %} 29 | 37 | 38 | {% else %} 39 | 44 | 45 | {% endif %} -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | &:focus { 3 | outline: none; 4 | } 5 | 6 | @extend .z-depth-5; 7 | 8 | display: none; 9 | position: fixed; 10 | left: 0; 11 | right: 0; 12 | background-color: #fafafa; 13 | padding: 0; 14 | max-height: 70%; 15 | width: 55%; 16 | margin: auto; 17 | overflow-y: auto; 18 | 19 | border-radius: 2px; 20 | will-change: top, opacity; 21 | 22 | @media #{$medium-and-down} { 23 | width: 80%; 24 | } 25 | 26 | h1,h2,h3,h4 { 27 | margin-top: 0; 28 | } 29 | 30 | .modal-content { 31 | padding: 24px; 32 | } 33 | .modal-close { 34 | cursor: pointer; 35 | } 36 | 37 | .modal-footer { 38 | border-radius: 0 0 2px 2px; 39 | background-color: #fafafa; 40 | padding: 4px 6px; 41 | height: 56px; 42 | width: 100%; 43 | text-align: right; 44 | 45 | .btn, .btn-flat { 46 | margin: 6px 0; 47 | } 48 | } 49 | } 50 | .modal-overlay { 51 | position: fixed; 52 | z-index: 999; 53 | top: -25%; 54 | left: 0; 55 | bottom: 0; 56 | right: 0; 57 | height: 125%; 58 | width: 100%; 59 | background: #000; 60 | display: none; 61 | 62 | will-change: opacity; 63 | } 64 | 65 | // Modal with fixed action footer 66 | .modal.modal-fixed-footer { 67 | padding: 0; 68 | height: 70%; 69 | 70 | .modal-content { 71 | position: absolute; 72 | height: calc(100% - 56px); 73 | max-height: 100%; 74 | width: 100%; 75 | overflow-y: auto; 76 | } 77 | 78 | .modal-footer { 79 | border-top: 1px solid rgba(0,0,0,.1); 80 | position: absolute; 81 | bottom: 0; 82 | } 83 | } 84 | 85 | // Modal Bottom Sheet Style 86 | .modal.bottom-sheet { 87 | top: auto; 88 | bottom: -100%; 89 | margin: 0; 90 | width: 100%; 91 | max-height: 45%; 92 | border-radius: 0; 93 | will-change: bottom, opacity; 94 | } 95 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-content { 2 | &:focus { 3 | outline: 0; 4 | } 5 | 6 | 7 | @extend .z-depth-1; 8 | background-color: $dropdown-bg-color; 9 | margin: 0; 10 | display: none; 11 | min-width: 100px; 12 | overflow-y: auto; 13 | opacity: 0; 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | z-index: 9999; // TODO: Check if this doesn't break other things 18 | transform-origin: 0 0; 19 | 20 | 21 | li { 22 | &:hover, &.active { 23 | background-color: $dropdown-hover-bg-color; 24 | } 25 | 26 | &:focus { 27 | outline: none; 28 | } 29 | 30 | &.divider { 31 | min-height: 0; 32 | height: 1px; 33 | } 34 | 35 | & > a, & > span { 36 | font-size: 16px; 37 | color: $dropdown-color; 38 | display: block; 39 | line-height: 22px; 40 | padding: (($dropdown-item-height - 22) / 2) 16px; 41 | } 42 | 43 | & > span > label { 44 | top: 1px; 45 | left: 0; 46 | height: 18px; 47 | } 48 | 49 | // Icon alignment override 50 | & > a > i { 51 | height: inherit; 52 | line-height: inherit; 53 | float: left; 54 | margin: 0 24px 0 0; 55 | width: 24px; 56 | } 57 | 58 | 59 | clear: both; 60 | color: $off-black; 61 | cursor: pointer; 62 | min-height: $dropdown-item-height; 63 | line-height: 1.5rem; 64 | width: 100%; 65 | text-align: left; 66 | } 67 | } 68 | 69 | body.keyboard-focused { 70 | .dropdown-content li:focus { 71 | background-color: darken($dropdown-hover-bg-color, 8%); 72 | } 73 | } 74 | 75 | // Input field specificity bugfix 76 | .input-field.col .dropdown-content [type="checkbox"] + label { 77 | top: 1px; 78 | left: 0; 79 | height: 18px; 80 | transform: none; 81 | } 82 | 83 | .dropdown-trigger { 84 | cursor: pointer; 85 | } -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_typography.scss: -------------------------------------------------------------------------------- 1 | 2 | a { 3 | text-decoration: none; 4 | } 5 | 6 | html{ 7 | line-height: 1.5; 8 | 9 | @media only screen and (min-width: 0) { 10 | font-size: 14px; 11 | } 12 | 13 | @media only screen and (min-width: $medium-screen) { 14 | font-size: 14.5px; 15 | } 16 | 17 | @media only screen and (min-width: $large-screen) { 18 | font-size: 15px; 19 | } 20 | 21 | font-family: $font-stack; 22 | font-weight: normal; 23 | color: $off-black; 24 | } 25 | h1, h2, h3, h4, h5, h6 { 26 | font-weight: 400; 27 | line-height: 1.3; 28 | } 29 | 30 | // Header Styles 31 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; } 32 | h1 { font-size: $h1-fontsize; line-height: 110%; margin: ($h1-fontsize / 1.5) 0 ($h1-fontsize / 2.5) 0;} 33 | h2 { font-size: $h2-fontsize; line-height: 110%; margin: ($h2-fontsize / 1.5) 0 ($h2-fontsize / 2.5) 0;} 34 | h3 { font-size: $h3-fontsize; line-height: 110%; margin: ($h3-fontsize / 1.5) 0 ($h3-fontsize / 2.5) 0;} 35 | h4 { font-size: $h4-fontsize; line-height: 110%; margin: ($h4-fontsize / 1.5) 0 ($h4-fontsize / 2.5) 0;} 36 | h5 { font-size: $h5-fontsize; line-height: 110%; margin: ($h5-fontsize / 1.5) 0 ($h5-fontsize / 2.5) 0;} 37 | h6 { font-size: $h6-fontsize; line-height: 110%; margin: ($h6-fontsize / 1.5) 0 ($h6-fontsize / 2.5) 0;} 38 | 39 | // Text Styles 40 | em { font-style: italic; } 41 | strong { font-weight: 500; } 42 | small { font-size: 75%; } 43 | .light { font-weight: 300; } 44 | .thin { font-weight: 200; } 45 | 46 | 47 | .flow-text{ 48 | $i: 0; 49 | @while $i <= $intervals { 50 | @media only screen and (min-width : 360 + ($i * $interval-size)) { 51 | font-size: 1.2rem * (1 + (.02 * $i)); 52 | } 53 | $i: $i + 1; 54 | } 55 | 56 | // Handle below 360px screen 57 | @media only screen and (max-width: 360px) { 58 | font-size: 1.2rem; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_slider.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | position: relative; 3 | height: 400px; 4 | width: 100%; 5 | 6 | // Fullscreen slider 7 | &.fullscreen { 8 | height: 100%; 9 | width: 100%; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | 16 | ul.slides { 17 | height: 100%; 18 | } 19 | 20 | ul.indicators { 21 | z-index: 2; 22 | bottom: 30px; 23 | } 24 | } 25 | 26 | .slides { 27 | background-color: $slider-bg-color; 28 | margin: 0; 29 | height: 400px; 30 | 31 | li { 32 | opacity: 0; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | z-index: 1; 37 | width: 100%; 38 | height: inherit; 39 | overflow: hidden; 40 | 41 | img { 42 | height: 100%; 43 | width: 100%; 44 | background-size: cover; 45 | background-position: center; 46 | } 47 | 48 | .caption { 49 | color: #fff; 50 | position: absolute; 51 | top: 15%; 52 | left: 15%; 53 | width: 70%; 54 | opacity: 0; 55 | 56 | p { color: $slider-bg-color-light; } 57 | } 58 | 59 | &.active { 60 | z-index: 2; 61 | } 62 | } 63 | } 64 | 65 | 66 | .indicators { 67 | position: absolute; 68 | text-align: center; 69 | left: 0; 70 | right: 0; 71 | bottom: 0; 72 | margin: 0; 73 | 74 | .indicator-item { 75 | display: inline-block; 76 | position: relative; 77 | cursor: pointer; 78 | height: 16px; 79 | width: 16px; 80 | margin: 0 12px; 81 | background-color: $slider-bg-color-light; 82 | 83 | transition: background-color .3s; 84 | border-radius: 50%; 85 | 86 | &.active { 87 | background-color: $slider-indicator-color; 88 | } 89 | } 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /apps/fok/templates/fok/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% load staticfiles %} 4 | {% load section %} 5 |
6 |
7 |
8 |

{{ config.FRONTPAGE_SENTENCE }}

9 | {% if user.is_anonymous %} 10 | {% include 'fok/partials/orcid_button.html' %} 11 | {% endif %} 12 |
13 |
14 | {% if config.FRONTPAGE_VIDEO_URL %} 15 | 18 | {% endif %} 19 |
20 |
21 |
22 |
23 |
24 |
25 |

{{ config.INTRODUCTION_TITLE_IN_FRONTPAGE }}

26 |
27 |
28 |
29 |
30 | {{ "introduction"|section }} 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |

{{ config.CAMPAIGNS_TITLE_IN_FRONTPAGE }}

39 |
40 |
41 |
42 |
43 | {{ "campaigns"|section }} 44 |
45 |
46 |
47 |
48 | {% for campaign in campaigns %} 49 | {% include 'fok/partials/campaign_widget.html' with campaign=campaign %} 50 | {% endfor %} 51 |
52 | 53 | {% endblock %} -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_carousel.scss: -------------------------------------------------------------------------------- 1 | .carousel { 2 | &.carousel-slider { 3 | top: 0; 4 | left: 0; 5 | 6 | .carousel-fixed-item { 7 | &.with-indicators { 8 | bottom: 68px; 9 | } 10 | 11 | position: absolute; 12 | left: 0; 13 | right: 0; 14 | bottom: 20px; 15 | z-index: 1; 16 | } 17 | 18 | .carousel-item { 19 | width: 100%; 20 | height: 100%; 21 | min-height: $carousel-height; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | 26 | h2 { 27 | font-size: 24px; 28 | font-weight: 500; 29 | line-height: 32px; 30 | } 31 | 32 | p { 33 | font-size: 15px; 34 | } 35 | } 36 | } 37 | 38 | overflow: hidden; 39 | position: relative; 40 | width: 100%; 41 | height: $carousel-height; 42 | perspective: 500px; 43 | transform-style: preserve-3d; 44 | transform-origin: 0% 50%; 45 | 46 | .carousel-item { 47 | visibility: hidden; 48 | width: $carousel-item-width; 49 | height: $carousel-item-height; 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | 54 | & > img { 55 | width: 100%; 56 | } 57 | } 58 | 59 | .indicators { 60 | position: absolute; 61 | text-align: center; 62 | left: 0; 63 | right: 0; 64 | bottom: 0; 65 | margin: 0; 66 | 67 | .indicator-item { 68 | &.active { 69 | background-color: #fff; 70 | } 71 | 72 | display: inline-block; 73 | position: relative; 74 | cursor: pointer; 75 | height: 8px; 76 | width: 8px; 77 | margin: 24px 4px; 78 | background-color: rgba(255,255,255,.5); 79 | 80 | transition: background-color .3s; 81 | border-radius: 50%; 82 | } 83 | } 84 | 85 | // Materialbox compatibility 86 | &.scrolling .carousel-item .materialboxed, 87 | .carousel-item:not(.active) .materialboxed { 88 | pointer-events: none; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_chips.scss: -------------------------------------------------------------------------------- 1 | .chip { 2 | &:focus { 3 | outline: none; 4 | background-color: $chip-selected-color; 5 | color: #fff; 6 | } 7 | 8 | display: inline-block; 9 | height: 32px; 10 | font-size: 13px; 11 | font-weight: 500; 12 | color: rgba(0,0,0,.6); 13 | line-height: 32px; 14 | padding: 0 12px; 15 | border-radius: 16px; 16 | background-color: $chip-bg-color; 17 | margin-bottom: $chip-margin; 18 | margin-right: $chip-margin; 19 | 20 | > img { 21 | float: left; 22 | margin: 0 8px 0 -12px; 23 | height: 32px; 24 | width: 32px; 25 | border-radius: 50%; 26 | } 27 | 28 | .close { 29 | cursor: pointer; 30 | float: right; 31 | font-size: 16px; 32 | line-height: 32px; 33 | padding-left: 8px; 34 | } 35 | } 36 | 37 | .chips { 38 | border: none; 39 | border-bottom: 1px solid $chip-border-color; 40 | box-shadow: none; 41 | margin: $input-margin; 42 | min-height: 45px; 43 | outline: none; 44 | transition: all .3s; 45 | 46 | &.focus { 47 | border-bottom: 1px solid $chip-selected-color; 48 | box-shadow: 0 1px 0 0 $chip-selected-color; 49 | } 50 | 51 | &:hover { 52 | cursor: text; 53 | } 54 | 55 | .input { 56 | background: none; 57 | border: 0; 58 | color: rgba(0,0,0,.6); 59 | display: inline-block; 60 | font-size: $input-font-size; 61 | height: $input-height; 62 | line-height: 32px; 63 | outline: 0; 64 | margin: 0; 65 | padding: 0 !important; 66 | width: 120px !important; 67 | } 68 | 69 | .input:focus { 70 | border: 0 !important; 71 | box-shadow: none !important; 72 | } 73 | 74 | // Autocomplete 75 | .autocomplete-content { 76 | margin-top: 0; 77 | margin-bottom: 0; 78 | } 79 | } 80 | 81 | // Form prefix 82 | .prefix ~ .chips { 83 | margin-left: 3rem; 84 | width: 92%; 85 | width: calc(100% - 3rem); 86 | } 87 | .chips:empty ~ label { 88 | font-size: 0.8rem; 89 | transform: translateY(-140%); 90 | } 91 | -------------------------------------------------------------------------------- /apps/cc_cms/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from uuid import uuid4 3 | from cc_lib.utils import implement_slug, truncate_text 4 | 5 | 6 | def upload_path(instance, filename): 7 | if isinstance(instance, Page): 8 | return 'course.banner/{0}/banner.png'.format(str(uuid4()), filename) 9 | 10 | 11 | class Content(models.Model): 12 | text = models.TextField() 13 | created_on = models.DateTimeField(null=True, blank=True, editable=False) 14 | 15 | def __str__(self): 16 | return truncate_text(self.text) 17 | 18 | 19 | class DynamicText(Content): 20 | name = models.CharField(max_length=100, blank=False, unique=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class Page(Content): 27 | name = models.CharField(max_length=100, blank=False, unique=True) 28 | title = models.CharField(max_length=200, blank=False) 29 | intro = models.TextField(null=True, blank=True) 30 | slug = models.CharField(max_length=250, unique=True, editable=False) 31 | thumbnail = models.ImageField(null=True, blank=True, upload_to=upload_path, max_length=250) 32 | 33 | def __str__(self): 34 | return self.name 35 | 36 | 37 | implement_slug(Page, 'name') 38 | 39 | 40 | class Section(models.Model): 41 | class Meta: 42 | abstract = True 43 | SECTION_TYPE = [ 44 | ('NON', 'none'), 45 | ('COL', 'columns'), 46 | ] 47 | name = models.CharField(max_length=200, blank=False, unique=True) 48 | type = models.CharField(max_length=3, choices=SECTION_TYPE, default='NON') 49 | content = models.ManyToManyField(Content, blank=True, related_name='sections') 50 | 51 | @staticmethod 52 | def query(**kwargs): 53 | subclss = Section.__subclasses__() 54 | for clss in subclss: 55 | try: 56 | obj = clss.objects.get(**kwargs) 57 | except clss.DoesNotExist: 58 | continue 59 | else: 60 | return obj 61 | return None 62 | 63 | def __str__(self): 64 | return self.name 65 | 66 | 67 | class ColumnsSection(Section): 68 | type = models.CharField(max_length=3, default='COL') 69 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_tabs.scss: -------------------------------------------------------------------------------- 1 | .tabs { 2 | &.tabs-transparent { 3 | background-color: transparent; 4 | 5 | .tab a, 6 | .tab.disabled a, 7 | .tab.disabled a:hover { 8 | color: rgba(255,255,255,0.7); 9 | } 10 | 11 | .tab a:hover, 12 | .tab a.active { 13 | color: #fff; 14 | } 15 | 16 | .indicator { 17 | background-color: #fff; 18 | } 19 | } 20 | 21 | &.tabs-fixed-width { 22 | display: flex; 23 | 24 | .tab { 25 | flex-grow: 1; 26 | } 27 | } 28 | 29 | position: relative; 30 | overflow-x: auto; 31 | overflow-y: hidden; 32 | height: 48px; 33 | width: 100%; 34 | background-color: $tabs-bg-color; 35 | margin: 0 auto; 36 | white-space: nowrap; 37 | 38 | .tab { 39 | display: inline-block; 40 | text-align: center; 41 | line-height: 48px; 42 | height: 48px; 43 | padding: 0; 44 | margin: 0; 45 | text-transform: uppercase; 46 | 47 | a { 48 | &:focus, 49 | &:focus.active { 50 | background-color: transparentize($tabs-underline-color, .8); 51 | outline: none; 52 | } 53 | 54 | &:hover, 55 | &.active { 56 | background-color: transparent; 57 | color: $tabs-text-color; 58 | } 59 | 60 | color: rgba($tabs-text-color, .7); 61 | display: block; 62 | width: 100%; 63 | height: 100%; 64 | padding: 0 24px; 65 | font-size: 14px; 66 | text-overflow: ellipsis; 67 | overflow: hidden; 68 | transition: color .28s ease, background-color .28s ease; 69 | } 70 | 71 | &.disabled a, 72 | &.disabled a:hover { 73 | color: rgba($tabs-text-color, .4); 74 | cursor: default; 75 | } 76 | } 77 | .indicator { 78 | position: absolute; 79 | bottom: 0; 80 | height: 2px; 81 | background-color: $tabs-underline-color; 82 | will-change: left, right; 83 | } 84 | } 85 | 86 | // Fixed Sidenav hide on smaller 87 | @media #{$medium-and-down} { 88 | .tabs { 89 | display: flex; 90 | 91 | .tab { 92 | flex-grow: 1; 93 | 94 | a { 95 | padding: 0 12px; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /apps/fok/views/SignPledgeView.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.views.generic import TemplateView 5 | from django.shortcuts import redirect, reverse 6 | from ..forms import SignPledgeForm 7 | from ..models import Pledge, Campaign 8 | from django.contrib import messages 9 | 10 | 11 | class SignPledgeView(TemplateView): 12 | template_name = "fok/pledge_detail.html" 13 | 14 | def get_context_data(self, **kwargs): 15 | campaign = Campaign.objects.get(slug=kwargs['slug']) 16 | pledge = campaign.get_pledge_from(self.request.user) 17 | context = super().get_context_data() 18 | context['object'] = campaign 19 | context['pledge'] = pledge 20 | context['form'] = SignPledgeForm(initial={'author_position': [] if not pledge else pledge.author_position.all()}) 21 | return context 22 | 23 | def post(self, request, *args, **kwargs): 24 | form = SignPledgeForm(request.POST) 25 | try: 26 | if not form.is_valid(): 27 | raise Exception('') 28 | campaign = Campaign.objects.get(slug=kwargs['slug']) 29 | pledge = campaign.get_pledge_from(request.user) 30 | author_position = form.cleaned_data.pop('author_position') 31 | if pledge is None: 32 | pledge = Pledge( 33 | user=request.user, 34 | campaign=campaign, 35 | **form.cleaned_data 36 | ) 37 | else: 38 | assert form.cleaned_data['implication'] <= pledge.implication 39 | for attr, value in form.cleaned_data.items(): 40 | setattr(pledge, attr, value) 41 | pledge.save() 42 | pledge.author_position.clear() 43 | for position in author_position: 44 | pledge.author_position.add(position) 45 | messages.success(request, 'The pledge was successfully signed') 46 | return redirect(reverse('campaign', kwargs={'slug': kwargs['slug']})) 47 | except Exception as e: 48 | messages.error(request, 'There was an error and this pledge could not be signed') 49 | return redirect(reverse('campaign', kwargs={'slug': kwargs['slug']})) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTE: this repo is deprecated and has been superceded. the new platform is being built [here](https://github.com/FreeOurKnowledge/freeourknowledge.github.io) 2 | 3 | 4 | # Free Our Knowledge: Platform code repository 5 | ### Vision Statement 6 | We're building a platform that helps researchers use collective action to establish positive cultural norms in academia, so that individuals can adopt open and reproducible research practices without risking their careers in the process. 7 | 8 | ### Project structure 9 | Project FOK is divided into two separate repositories: 10 | * (1) this 'platform' repository, which is for developing the code behind the [website](https://www.freeourknowledge.org/) 11 | * (2) a ['community' repository](https://github.com/FreeOurKnowledge/community), which is for storing documents (e.g., marketing materials) and organising discussions about the project (including the design of new campaigns, using the [Issues feature](https://github.com/FreeOurKnowledge/documentation/issues/new/choose)). Note that the project [roadmap](https://github.com/FreeOurKnowledge/community/blob/master/ROADMAP.md) is housed over there. 12 | 13 | ### How you can support Project FOK 14 | 1. Help us by doing the following: 15 | * Watch/star this repository (and if interested, the [platform repository](https://github.com/FreeOurKnowledge/platform/)) 16 | * Join our [mailing list](http://eepurl.com/dFVBVz) 17 | * Log in to the [website](https://www.freeourknowledge.org/) and pledge to any [campaigns](https://www.freeourknowledge.org/#campaigns) that you support 18 | * Follow/retweet us on [Twitter](https://twitter.com/projectfok) and [Facebook](https://www.facebook.com/projectFOK/) 19 | * Join our [Google Group](https://groups.google.com/g/free-our-knowledge-community/) 20 | * Share our [GofundMe campaign](gf.me/u/yvgtgg) (and donate if you can afford to :)) 21 | 2. If you're a developer, take a look at the Issues in this repository to see what needs work. The [roadmap in the community repo](https://github.com/FreeOurKnowledge/community/blob/master/ROADMAP.md) should hopefully clarify what takes priority. 22 | 3. If you're not a developer, take a look at the [readme in the community repository](https://github.com/FreeOurKnowledge/community/blob/master/README.md) to see how you can help :) 23 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_collapsible.scss: -------------------------------------------------------------------------------- 1 | .collapsible { 2 | border-top: 1px solid $collapsible-border-color; 3 | border-right: 1px solid $collapsible-border-color; 4 | border-left: 1px solid $collapsible-border-color; 5 | margin: $element-top-margin 0 $element-bottom-margin 0; 6 | @extend .z-depth-1; 7 | } 8 | 9 | .collapsible-header { 10 | &:focus { 11 | outline: 0 12 | } 13 | 14 | display: flex; 15 | cursor: pointer; 16 | -webkit-tap-highlight-color: transparent; 17 | line-height: 1.5; 18 | padding: 1rem; 19 | background-color: $collapsible-header-color; 20 | border-bottom: 1px solid $collapsible-border-color; 21 | 22 | i { 23 | width: 2rem; 24 | font-size: 1.6rem; 25 | display: inline-block; 26 | text-align: center; 27 | margin-right: 1rem; 28 | } 29 | } 30 | .keyboard-focused .collapsible-header:focus { 31 | background-color: #eee; 32 | } 33 | 34 | .collapsible-body { 35 | display: none; 36 | border-bottom: 1px solid $collapsible-border-color; 37 | box-sizing: border-box; 38 | padding: 2rem; 39 | } 40 | 41 | // Sidenav collapsible styling 42 | .sidenav, 43 | .sidenav.fixed { 44 | 45 | .collapsible { 46 | border: none; 47 | box-shadow: none; 48 | 49 | li { padding: 0; } 50 | } 51 | 52 | .collapsible-header { 53 | background-color: transparent; 54 | border: none; 55 | line-height: inherit; 56 | height: inherit; 57 | padding: 0 $sidenav-padding; 58 | 59 | &:hover { background-color: rgba(0,0,0,.05); } 60 | i { line-height: inherit; } 61 | } 62 | 63 | .collapsible-body { 64 | border: 0; 65 | background-color: $collapsible-header-color; 66 | 67 | li a { 68 | padding: 0 (7.5px + $sidenav-padding) 69 | 0 (15px + $sidenav-padding); 70 | } 71 | } 72 | 73 | } 74 | 75 | // Popout Collapsible 76 | 77 | .collapsible.popout { 78 | border: none; 79 | box-shadow: none; 80 | > li { 81 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 82 | // transform: scaleX(.92); 83 | margin: 0 24px; 84 | transition: margin .35s cubic-bezier(0.250, 0.460, 0.450, 0.940); 85 | } 86 | > li.active { 87 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); 88 | margin: 16px 0; 89 | // transform: scaleX(1); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cc_lib/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def slugify_model(instance, attrib, slug_attrib='slug', _iter=0): 6 | from django.utils.text import slugify 7 | value = getattr(instance, attrib) 8 | assert isinstance(value, str) 9 | prev_value = getattr(instance, slug_attrib) 10 | if prev_value.startswith(slugify(value)): 11 | return 12 | _value = slugify(value) if _iter == 0 else slugify(f'{value}-{_iter}') 13 | params = {slug_attrib: _value} 14 | obj_is_new = instance._meta.model.objects.filter(**params).first() is None 15 | setattr(instance, slug_attrib, _value) if obj_is_new else slugify_model(instance, attrib, slug_attrib, _iter + 1) 16 | 17 | 18 | def implement_slug(clss, attrib_from, slug_attrib='slug'): 19 | # TODO: Create a Model Field slug that auto stores the value 20 | @classmethod 21 | def generate_slug(cls, sender, instance, **kwargs): 22 | slugify_model(instance, attrib_from, slug_attrib=slug_attrib) 23 | 24 | clss.generate_slug = generate_slug 25 | from django.db.models.signals import pre_save 26 | pre_save.connect(clss.generate_slug, sender=clss) 27 | 28 | 29 | def storage_files(folder, prefix=None): 30 | import boto3 31 | from django.conf import settings 32 | from urllib.parse import urljoin 33 | 34 | session = boto3.session.Session() 35 | s3_client = session.client( 36 | service_name='s3', 37 | aws_access_key_id=settings.AWS_ACCESS_KEY_ID, 38 | aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, 39 | endpoint_url=settings.AWS_S3_ENDPOINT_URL, 40 | ) 41 | response = s3_client.list_objects(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Prefix=folder) 42 | return [ 43 | urljoin(prefix, obj['Key']) if prefix else obj['Key'] 44 | for obj in response['Contents'] 45 | if obj['Key'] and obj['Size'] > 0 46 | ] 47 | 48 | 49 | def get_class_from_route(route): 50 | """ 51 | From a Python class path in string format, returns the class 52 | """ 53 | from importlib import import_module 54 | values = route.split('.') 55 | module = import_module('.'.join(values[:-1])) 56 | cl = getattr(module, values[-1]) 57 | return cl 58 | 59 | 60 | def truncate_text(text, size=100): 61 | return text[:size] + '...' if len(text) > size else text 62 | -------------------------------------------------------------------------------- /cc_lib/fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from factory.django import DjangoModelFactory 5 | import factory 6 | import cc_lib.fixture_helpers as helpers 7 | 8 | 9 | known_attributes = { 10 | 'address': factory.Faker('address'), 11 | 'date_joined': helpers.one_past_moment_between_days(100, 10), 12 | 'description': factory.Faker('text', max_nb_chars=2000), 13 | 'email': factory.Sequence(lambda n: "user%d@codi.coop" % n), 14 | 'first_name': factory.Faker('first_name'), 15 | 'is_active': True, 16 | 'is_staff': False, 17 | 'is_superuser': False, 18 | 'last_login': helpers.one_moment_in_the_last_days(10), 19 | 'last_name': factory.Faker('last_name'), 20 | 'name': lambda cls_name: factory.Sequence(lambda n: f'{cls_name} {n}'), 21 | 'password': factory.PostGenerationMethodCall('set_password', 'test'), 22 | 'phone': factory.Faker('phone_number'), 23 | 'text': factory.Faker('text', max_nb_chars=2000), 24 | 'title': factory.Faker('sentence'), 25 | 'url': factory.Faker('url'), 26 | 'username': factory.Sequence(lambda n: "user%d" % n), 27 | 'web': factory.Faker('url'), 28 | } 29 | 30 | 31 | def set_att_value(cls, field): 32 | the_field = known_attributes[field.attname] 33 | the_field = the_field \ 34 | if not hasattr(known_attributes[field.attname], '__call__') \ 35 | else the_field(cls._meta.model.__name__) 36 | cls._meta.pre_declarations.declarations[field.attname] = the_field 37 | if hasattr(the_field, 'unroll_context') and hasattr(the_field, 'call'): 38 | cls._meta.post_declarations.declarations[field.attname] = known_attributes[field.attname] 39 | setattr(cls, field.attname, the_field) 40 | cls._meta.base_declarations[field.attname] = the_field 41 | 42 | 43 | class DjangoFactory(DjangoModelFactory): 44 | class Meta: 45 | abstract = True 46 | 47 | @classmethod 48 | def _generate(cls, strategy, params): 49 | fields = cls._meta.model._meta.get_fields(True, True) 50 | for field in fields: 51 | # is a field that has attname not None and has not been previously declared, then set attribute 52 | hasattr(field, 'attname') \ 53 | and known_attributes.get(field.attname, None) \ 54 | and not cls._meta.pre_declarations.declarations.get(field.attname, None) \ 55 | and set_att_value(cls, field) 56 | return super(DjangoFactory, cls)._generate(strategy, params) 57 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_switches.scss: -------------------------------------------------------------------------------- 1 | /* Switch 2 | ========================================================================== */ 3 | 4 | .switch, 5 | .switch * { 6 | -webkit-tap-highlight-color: transparent; 7 | user-select: none; 8 | } 9 | 10 | .switch label { 11 | cursor: pointer; 12 | } 13 | 14 | .switch label input[type=checkbox] { 15 | opacity: 0; 16 | width: 0; 17 | height: 0; 18 | 19 | &:checked + .lever { 20 | background-color: $switch-checked-lever-bg; 21 | 22 | &:before, &:after { 23 | left: 18px; 24 | } 25 | 26 | &:after { 27 | background-color: $switch-bg-color; 28 | } 29 | } 30 | } 31 | 32 | .switch label .lever { 33 | content: ""; 34 | display: inline-block; 35 | position: relative; 36 | width: 36px; 37 | height: 14px; 38 | background-color: $switch-unchecked-lever-bg; 39 | border-radius: $switch-radius; 40 | margin-right: 10px; 41 | transition: background 0.3s ease; 42 | vertical-align: middle; 43 | margin: 0 16px; 44 | 45 | &:before, &:after { 46 | content: ""; 47 | position: absolute; 48 | display: inline-block; 49 | width: 20px; 50 | height: 20px; 51 | border-radius: 50%; 52 | left: 0; 53 | top: -3px; 54 | transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease; 55 | } 56 | 57 | &:before { 58 | background-color: transparentize($switch-bg-color, .85); 59 | } 60 | 61 | &:after { 62 | background-color: $switch-unchecked-bg; 63 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 64 | } 65 | } 66 | 67 | // Switch active style 68 | input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before, 69 | input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before { 70 | transform: scale(2.4); 71 | background-color: transparentize($switch-bg-color, .85); 72 | } 73 | 74 | input[type=checkbox]:not(:disabled) ~ .lever:active:before, 75 | input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before { 76 | transform: scale(2.4); 77 | background-color: rgba(0,0,0,.08); 78 | } 79 | 80 | // Disabled Styles 81 | .switch input[type=checkbox][disabled] + .lever { 82 | cursor: default; 83 | background-color: rgba(0,0,0,.12); 84 | } 85 | 86 | .switch label input[type=checkbox][disabled] + .lever:after, 87 | .switch label input[type=checkbox][disabled]:checked + .lever:after { 88 | background-color: $input-disabled-solid-color; 89 | } 90 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_tapTarget.scss: -------------------------------------------------------------------------------- 1 | .tap-target-wrapper { 2 | width: 800px; 3 | height: 800px; 4 | position: fixed; 5 | z-index: 1000; 6 | visibility: hidden; 7 | transition: visibility 0s .3s; 8 | } 9 | 10 | .tap-target-wrapper.open { 11 | visibility: visible; 12 | transition: visibility 0s; 13 | 14 | .tap-target { 15 | transform: scale(1); 16 | opacity: .95; 17 | transition: 18 | transform .3s cubic-bezier(.42,0,.58,1), 19 | opacity .3s cubic-bezier(.42,0,.58,1); 20 | } 21 | 22 | .tap-target-wave::before { 23 | transform: scale(1); 24 | } 25 | .tap-target-wave::after { 26 | visibility: visible; 27 | animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; 28 | transition: 29 | opacity .3s, 30 | transform .3s, 31 | visibility 0s 1s; 32 | } 33 | } 34 | 35 | .tap-target { 36 | position: absolute; 37 | font-size: 1rem; 38 | border-radius: 50%; 39 | background-color: $primary-color; 40 | box-shadow: 0 20px 20px 0 rgba(0,0,0,0.14), 0 10px 50px 0 rgba(0,0,0,0.12), 0 30px 10px -20px rgba(0,0,0,0.2); 41 | width: 100%; 42 | height: 100%; 43 | opacity: 0; 44 | transform: scale(0); 45 | transition: 46 | transform .3s cubic-bezier(.42,0,.58,1), 47 | opacity .3s cubic-bezier(.42,0,.58,1); 48 | } 49 | 50 | .tap-target-content { 51 | position: relative; 52 | display: table-cell; 53 | } 54 | 55 | .tap-target-wave { 56 | &::before, 57 | &::after { 58 | content: ''; 59 | display: block; 60 | position: absolute; 61 | width: 100%; 62 | height: 100%; 63 | border-radius: 50%; 64 | background-color: #ffffff; 65 | } 66 | &::before { 67 | transform: scale(0); 68 | transition: transform .3s; 69 | } 70 | &::after { 71 | visibility: hidden; 72 | transition: 73 | opacity .3s, 74 | transform .3s, 75 | visibility 0s; 76 | z-index: -1; 77 | } 78 | 79 | position: absolute; 80 | border-radius: 50%; 81 | z-index: 10001; 82 | } 83 | 84 | .tap-target-origin { 85 | &:not(.btn), 86 | &:not(.btn):hover { 87 | background: none; 88 | } 89 | 90 | top: 50%; 91 | left: 50%; 92 | transform: translate(-50%,-50%); 93 | 94 | z-index: 10002; 95 | position: absolute !important; 96 | } 97 | 98 | @media only screen and (max-width: 600px) { 99 | .tap-target, .tap-target-wrapper { 100 | width: 600px; 101 | height: 600px; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/cc_cms/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-07-06 07:24 2 | 3 | import cc_cms.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ColumnsSection', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=200, unique=True)), 21 | ('type', models.CharField(default='COL', max_length=3)), 22 | ], 23 | options={ 24 | 'abstract': False, 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='Content', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('text', models.TextField()), 32 | ('created_on', models.DateTimeField(blank=True, editable=False, null=True)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='DynamicText', 37 | fields=[ 38 | ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cc_cms.Content')), 39 | ('name', models.CharField(max_length=100, unique=True)), 40 | ], 41 | bases=('cc_cms.content',), 42 | ), 43 | migrations.CreateModel( 44 | name='Page', 45 | fields=[ 46 | ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cc_cms.Content')), 47 | ('name', models.CharField(max_length=100, unique=True)), 48 | ('title', models.CharField(max_length=200)), 49 | ('intro', models.TextField(blank=True, null=True)), 50 | ('slug', models.CharField(editable=False, max_length=250, unique=True)), 51 | ('thumbnail', models.ImageField(blank=True, max_length=250, null=True, upload_to=cc_cms.models.upload_path)), 52 | ], 53 | bases=('cc_cms.content',), 54 | ), 55 | migrations.AddField( 56 | model_name='columnssection', 57 | name='content', 58 | field=models.ManyToManyField(blank=True, related_name='sections', to='cc_cms.Content'), 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_waves.scss: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Waves v0.6.0 4 | * http://fian.my.id/Waves 5 | * 6 | * Copyright 2014 Alfiana E. Sibuea and other contributors 7 | * Released under the MIT license 8 | * https://github.com/fians/Waves/blob/master/LICENSE 9 | */ 10 | 11 | 12 | .waves-effect { 13 | position: relative; 14 | cursor: pointer; 15 | display: inline-block; 16 | overflow: hidden; 17 | user-select: none; 18 | -webkit-tap-highlight-color: transparent; 19 | vertical-align: middle; 20 | z-index: 1; 21 | transition: .3s ease-out; 22 | 23 | .waves-ripple { 24 | position: absolute; 25 | border-radius: 50%; 26 | width: 20px; 27 | height: 20px; 28 | margin-top:-10px; 29 | margin-left:-10px; 30 | opacity: 0; 31 | 32 | background: rgba(0,0,0,0.2); 33 | transition: all 0.7s ease-out; 34 | transition-property: transform, opacity; 35 | transform: scale(0); 36 | pointer-events: none; 37 | } 38 | 39 | // Waves Colors 40 | &.waves-light .waves-ripple { 41 | background-color: rgba(255, 255, 255, 0.45); 42 | } 43 | &.waves-red .waves-ripple { 44 | background-color: rgba(244, 67, 54, .70); 45 | } 46 | &.waves-yellow .waves-ripple { 47 | background-color: rgba(255, 235, 59, .70); 48 | } 49 | &.waves-orange .waves-ripple { 50 | background-color: rgba(255, 152, 0, .70); 51 | } 52 | &.waves-purple .waves-ripple { 53 | background-color: rgba(156, 39, 176, 0.70); 54 | } 55 | &.waves-green .waves-ripple { 56 | background-color: rgba(76, 175, 80, 0.70); 57 | } 58 | &.waves-teal .waves-ripple { 59 | background-color: rgba(0, 150, 136, 0.70); 60 | } 61 | 62 | // Style input button bug. 63 | input[type="button"], input[type="reset"], input[type="submit"] { 64 | border: 0; 65 | font-style: normal; 66 | font-size: inherit; 67 | text-transform: inherit; 68 | background: none; 69 | } 70 | 71 | img { 72 | position: relative; 73 | z-index: -1; 74 | } 75 | } 76 | 77 | .waves-notransition { 78 | transition: none #{"!important"}; 79 | } 80 | 81 | .waves-circle { 82 | transform: translateZ(0); 83 | -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); 84 | } 85 | 86 | .waves-input-wrapper { 87 | border-radius: 0.2em; 88 | vertical-align: bottom; 89 | 90 | .waves-button-input { 91 | position: relative; 92 | top: 0; 93 | left: 0; 94 | z-index: 1; 95 | } 96 | } 97 | 98 | .waves-circle { 99 | text-align: center; 100 | width: 2.5em; 101 | height: 2.5em; 102 | line-height: 2.5em; 103 | border-radius: 50%; 104 | -webkit-mask-image: none; 105 | } 106 | 107 | .waves-block { 108 | display: block; 109 | } 110 | 111 | /* Firefox Bug: link not triggered */ 112 | .waves-effect .waves-ripple { 113 | z-index: -1; 114 | } -------------------------------------------------------------------------------- /apps/fok/templates/fok/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if messages %} 6 | {% include 'fok/partials/messages.html' %} 7 | {% endif %} 8 | 9 |

My contribution

10 |
11 |
12 |
13 |
Personal information
14 |
15 | {% csrf_token %} 16 | 17 | {{ user_data_form }} 18 |
19 | 20 |
21 |
22 |
23 |
24 |
Other options
25 | 31 |
32 |
33 |
34 | {% if user.pledged_campaigns %}
Your pledges
{% endif %} 35 | {% for campaign in user.pledged_campaigns %} 36 |
37 |
38 |
39 |
40 |
{{ campaign.title }}
41 | arrow_backGo to campaign page 42 |
43 |
44 |
45 | {% endfor %} 46 |
47 |
48 | 49 | 66 | {% endblock %} -------------------------------------------------------------------------------- /cc_lib/form_fields.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django import forms 5 | from PIL import Image 6 | from io import BytesIO 7 | from django.forms.widgets import CheckboxInput 8 | 9 | 10 | class ExtendedImageField(forms.ImageField): 11 | def __init__(self, resize=None, filename=None, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.resize = resize 14 | self.filename = filename 15 | 16 | def to_python(self, data): 17 | f = super().to_python(data) 18 | if not self.resize: 19 | return f 20 | 21 | byte_array = BytesIO() 22 | image = Image.open(f) 23 | # image.show() 24 | image = image.resize(self.resize) 25 | image.save(byte_array, format='PNG') 26 | f.name = self.filename if self.filename else f.name 27 | f.file = byte_array 28 | f.content_type = Image.MIME.get(image.format) 29 | f.image = image 30 | return f 31 | 32 | 33 | class SelectMultipleChecks(forms.Widget): 34 | template_name = 'widgets/multiple_checks.html' 35 | 36 | def __init__(self, *args, choices=(), defaults=(), **kwargs): 37 | super().__init__(*args, **kwargs) 38 | self.choices = choices 39 | self.defaults = defaults 40 | 41 | def get_context(self, name, value, attrs): 42 | context = super().get_context(name, value, attrs) 43 | context['choices'] = self.choices 44 | context['selected'] = [v.pk for v in value] if value else [v.pk for v in self.defaults] 45 | return context 46 | 47 | 48 | class SelectMultipleChecksField(forms.Field): 49 | widget = SelectMultipleChecks 50 | 51 | def __init__(self, *args, choices=(), defaults=(), **kwargs): 52 | super().__init__(*args, **kwargs) 53 | self.choices = choices if choices else None 54 | self.defaults = defaults if defaults else None 55 | 56 | def _get_choices(self): 57 | return self._choices 58 | 59 | def _set_choices(self, value): 60 | self._choices = self.widget.choices = value if value else None 61 | 62 | choices = property(_get_choices, _set_choices) 63 | 64 | def _get_defaults(self): 65 | return self._defaults 66 | 67 | def _set_defaults(self, value): 68 | self._get_defaults = self.widget.defaults = value if value else None 69 | 70 | defaults = property(_get_defaults, _set_defaults) 71 | 72 | def to_python(self, value): 73 | import json 74 | values = json.loads(value) 75 | i_values = [int(v) for v in values] 76 | return [choice for choice in self._choices if choice.pk in i_values] 77 | 78 | def validate(self, value): 79 | assert len(value) > 0 80 | 81 | def valid_value(self, value): 82 | return value 83 | 84 | 85 | class SwitchWidget(CheckboxInput): 86 | input_type = 'checkbox' 87 | template_name = 'widgets/switch_widget.html' 88 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_radio-buttons.scss: -------------------------------------------------------------------------------- 1 | /* Radio Buttons 2 | ========================================================================== */ 3 | 4 | // Remove default Radio Buttons 5 | [type="radio"]:not(:checked), 6 | [type="radio"]:checked { 7 | position: absolute; 8 | opacity: 0; 9 | pointer-events: none; 10 | } 11 | 12 | [type="radio"]:not(:checked) + span, 13 | [type="radio"]:checked + span { 14 | position: relative; 15 | padding-left: 35px; 16 | cursor: pointer; 17 | display: inline-block; 18 | height: 25px; 19 | line-height: 25px; 20 | font-size: 1rem; 21 | transition: .28s ease; 22 | user-select: none; 23 | } 24 | 25 | [type="radio"] + span:before, 26 | [type="radio"] + span:after { 27 | content: ''; 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | margin: 4px; 32 | width: 16px; 33 | height: 16px; 34 | z-index: 0; 35 | transition: .28s ease; 36 | } 37 | 38 | /* Unchecked styles */ 39 | [type="radio"]:not(:checked) + span:before, 40 | [type="radio"]:not(:checked) + span:after, 41 | [type="radio"]:checked + span:before, 42 | [type="radio"]:checked + span:after, 43 | [type="radio"].with-gap:checked + span:before, 44 | [type="radio"].with-gap:checked + span:after { 45 | border-radius: 50%; 46 | } 47 | 48 | [type="radio"]:not(:checked) + span:before, 49 | [type="radio"]:not(:checked) + span:after { 50 | border: 2px solid $radio-empty-color; 51 | } 52 | 53 | [type="radio"]:not(:checked) + span:after { 54 | transform: scale(0); 55 | } 56 | 57 | /* Checked styles */ 58 | [type="radio"]:checked + span:before { 59 | border: 2px solid transparent; 60 | } 61 | 62 | [type="radio"]:checked + span:after, 63 | [type="radio"].with-gap:checked + span:before, 64 | [type="radio"].with-gap:checked + span:after { 65 | border: $radio-border; 66 | } 67 | 68 | [type="radio"]:checked + span:after, 69 | [type="radio"].with-gap:checked + span:after { 70 | background-color: $radio-fill-color; 71 | } 72 | 73 | [type="radio"]:checked + span:after { 74 | transform: scale(1.02); 75 | } 76 | 77 | /* Radio With gap */ 78 | [type="radio"].with-gap:checked + span:after { 79 | transform: scale(.5); 80 | } 81 | 82 | /* Focused styles */ 83 | [type="radio"].tabbed:focus + span:before { 84 | box-shadow: 0 0 0 10px rgba(0,0,0,.1); 85 | } 86 | 87 | /* Disabled Radio With gap */ 88 | [type="radio"].with-gap:disabled:checked + span:before { 89 | border: 2px solid $input-disabled-color; 90 | } 91 | 92 | [type="radio"].with-gap:disabled:checked + span:after { 93 | border: none; 94 | background-color: $input-disabled-color; 95 | } 96 | 97 | /* Disabled style */ 98 | [type="radio"]:disabled:not(:checked) + span:before, 99 | [type="radio"]:disabled:checked + span:before { 100 | background-color: transparent; 101 | border-color: $input-disabled-color; 102 | } 103 | 104 | [type="radio"]:disabled + span { 105 | color: $input-disabled-color; 106 | } 107 | 108 | [type="radio"]:disabled:not(:checked) + span:before { 109 | border-color: $input-disabled-color; 110 | } 111 | 112 | [type="radio"]:disabled:checked + span:after { 113 | background-color: $input-disabled-color; 114 | border-color: $input-disabled-solid-color; 115 | } 116 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_grid.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0 auto; 3 | max-width: 1280px; 4 | width: 90%; 5 | } 6 | @media #{$medium-and-up} { 7 | .container { 8 | width: 85%; 9 | } 10 | } 11 | @media #{$large-and-up} { 12 | .container { 13 | width: 70%; 14 | } 15 | } 16 | .col .row { 17 | margin-left: (-1 * $gutter-width / 2); 18 | margin-right: (-1 * $gutter-width / 2); 19 | } 20 | 21 | .section { 22 | padding-top: 1rem; 23 | padding-bottom: 1rem; 24 | 25 | &.no-pad { 26 | padding: 0; 27 | } 28 | &.no-pad-bot { 29 | padding-bottom: 0; 30 | } 31 | &.no-pad-top { 32 | padding-top: 0; 33 | } 34 | } 35 | 36 | 37 | // Mixins to eliminate code repitition 38 | @mixin reset-offset { 39 | margin-left: auto; 40 | left: auto; 41 | right: auto; 42 | } 43 | @mixin grid-classes($size, $i, $perc) { 44 | &.offset-#{$size}#{$i} { 45 | margin-left: $perc; 46 | } 47 | &.pull-#{$size}#{$i} { 48 | right: $perc; 49 | } 50 | &.push-#{$size}#{$i} { 51 | left: $perc; 52 | } 53 | } 54 | 55 | 56 | .row { 57 | margin-left: auto; 58 | margin-right: auto; 59 | margin-bottom: 20px; 60 | 61 | // Clear floating children 62 | &:after { 63 | content: ""; 64 | display: table; 65 | clear: both; 66 | } 67 | 68 | .col { 69 | float: left; 70 | box-sizing: border-box; 71 | padding: 0 $gutter-width / 2; 72 | min-height: 1px; 73 | 74 | &[class*="push-"], 75 | &[class*="pull-"] { 76 | position: relative; 77 | } 78 | 79 | $i: 1; 80 | @while $i <= $num-cols { 81 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 82 | &.s#{$i} { 83 | width: $perc; 84 | @include reset-offset; 85 | } 86 | $i: $i + 1; 87 | } 88 | 89 | $i: 1; 90 | @while $i <= $num-cols { 91 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 92 | @include grid-classes("s", $i, $perc); 93 | $i: $i + 1; 94 | } 95 | 96 | @media #{$medium-and-up} { 97 | 98 | $i: 1; 99 | @while $i <= $num-cols { 100 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 101 | &.m#{$i} { 102 | width: $perc; 103 | @include reset-offset; 104 | } 105 | $i: $i + 1 106 | } 107 | 108 | $i: 1; 109 | @while $i <= $num-cols { 110 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 111 | @include grid-classes("m", $i, $perc); 112 | $i: $i + 1; 113 | } 114 | } 115 | 116 | @media #{$large-and-up} { 117 | 118 | $i: 1; 119 | @while $i <= $num-cols { 120 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 121 | &.l#{$i} { 122 | width: $perc; 123 | @include reset-offset; 124 | } 125 | $i: $i + 1; 126 | } 127 | 128 | $i: 1; 129 | @while $i <= $num-cols { 130 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 131 | @include grid-classes("l", $i, $perc); 132 | $i: $i + 1; 133 | } 134 | } 135 | 136 | @media #{$extra-large-and-up} { 137 | 138 | $i: 1; 139 | @while $i <= $num-cols { 140 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 141 | &.xl#{$i} { 142 | width: $perc; 143 | @include reset-offset; 144 | } 145 | $i: $i + 1; 146 | } 147 | 148 | $i: 1; 149 | @while $i <= $num-cols { 150 | $perc: unquote((100 / ($num-cols / $i)) + "%"); 151 | @include grid-classes("xl", $i, $perc); 152 | $i: $i + 1; 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /apps/fok/templates/fok/campaign_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "generic.html" %} 2 | 3 | {% block generic %} 4 | 5 | 6 | 7 | {% if messages %} 8 | {% include 'fok/partials/messages.html' %} 9 | {% endif %} 10 | 11 | {% load staticfiles %} 12 | 23 | 24 |
25 |
26 | 27 |

{{ object.title }}

28 |
29 |
30 |
31 | Support 32 |

Total pledges: {{ object.stats.pledges_count }}

33 |

Support metric: {{ object.stats.support_metric }}%

34 |
35 |
36 | Public supporters 37 |
38 |
39 | 59 |
60 |
61 |
62 |

Rationale

63 | {{ object.description | safe }} 64 |

Criteria

65 | {{ object.criteria | safe }} 66 | {% if pledge %} 67 |
68 | 73 | Thanks for your support 74 |
75 | {% endif %} 76 | 77 |
78 | {% if user.is_anonymous %} 79 | Log in before pledging: 80 | {% include 'fok/partials/orcid_button.html' %} 81 | {% else %} 82 | {% if not pledge %} 83 | Pledge 84 | {% endif %} 85 | {% endif %} 86 |
87 |
88 |
89 | 90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /apps/fok/static/js/characterCounter.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | let _defaults = {}; 5 | 6 | /** 7 | * @class 8 | * 9 | */ 10 | class CharacterCounter extends Component { 11 | /** 12 | * Construct CharacterCounter instance 13 | * @constructor 14 | * @param {Element} el 15 | * @param {Object} options 16 | */ 17 | constructor(el, options) { 18 | super(CharacterCounter, el, options); 19 | 20 | this.el.M_CharacterCounter = this; 21 | 22 | /** 23 | * Options for the character counter 24 | */ 25 | this.options = $.extend({}, CharacterCounter.defaults, options); 26 | 27 | this.isInvalid = false; 28 | this.isValidLength = false; 29 | this._setupCounter(); 30 | this._setupEventHandlers(); 31 | } 32 | 33 | static get defaults() { 34 | return _defaults; 35 | } 36 | 37 | static init(els, options) { 38 | return super.init(this, els, options); 39 | } 40 | 41 | /** 42 | * Get Instance 43 | */ 44 | static getInstance(el) { 45 | let domElem = !!el.jquery ? el[0] : el; 46 | return domElem.M_CharacterCounter; 47 | } 48 | 49 | /** 50 | * Teardown component 51 | */ 52 | destroy() { 53 | this._removeEventHandlers(); 54 | this.el.CharacterCounter = undefined; 55 | this._removeCounter(); 56 | } 57 | 58 | /** 59 | * Setup Event Handlers 60 | */ 61 | _setupEventHandlers() { 62 | this._handleUpdateCounterBound = this.updateCounter.bind(this); 63 | 64 | this.el.addEventListener('focus', this._handleUpdateCounterBound, true); 65 | this.el.addEventListener('input', this._handleUpdateCounterBound, true); 66 | } 67 | 68 | /** 69 | * Remove Event Handlers 70 | */ 71 | _removeEventHandlers() { 72 | this.el.removeEventListener('focus', this._handleUpdateCounterBound, true); 73 | this.el.removeEventListener('input', this._handleUpdateCounterBound, true); 74 | } 75 | 76 | /** 77 | * Setup counter element 78 | */ 79 | _setupCounter() { 80 | this.counterEl = document.createElement('span'); 81 | $(this.counterEl) 82 | .addClass('character-counter') 83 | .css({ 84 | float: 'right', 85 | 'font-size': '12px', 86 | height: 1 87 | }); 88 | 89 | this.$el.parent().append(this.counterEl); 90 | } 91 | 92 | /** 93 | * Remove counter element 94 | */ 95 | _removeCounter() { 96 | $(this.counterEl).remove(); 97 | } 98 | 99 | /** 100 | * Update counter 101 | */ 102 | updateCounter() { 103 | let maxLength = +this.$el.attr('data-length'), 104 | actualLength = this.el.value.length; 105 | this.isValidLength = actualLength <= maxLength; 106 | let counterString = actualLength; 107 | 108 | if (maxLength) { 109 | counterString += '/' + maxLength; 110 | this._validateInput(); 111 | } 112 | 113 | $(this.counterEl).html(counterString); 114 | } 115 | 116 | /** 117 | * Add validation classes 118 | */ 119 | _validateInput() { 120 | if (this.isValidLength && this.isInvalid) { 121 | this.isInvalid = false; 122 | this.$el.removeClass('invalid'); 123 | } else if (!this.isValidLength && !this.isInvalid) { 124 | this.isInvalid = true; 125 | this.$el.removeClass('valid'); 126 | this.$el.addClass('invalid'); 127 | } 128 | } 129 | } 130 | 131 | M.CharacterCounter = CharacterCounter; 132 | 133 | if (M.jQueryLoaded) { 134 | M.initializeJqueryWrapper(CharacterCounter, 'characterCounter', 'M_CharacterCounter'); 135 | } 136 | })(cash); 137 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_datepicker.scss: -------------------------------------------------------------------------------- 1 | /* Modal */ 2 | .datepicker-modal { 3 | max-width: 325px; 4 | min-width: 300px; 5 | max-height: none; 6 | } 7 | 8 | .datepicker-container.modal-content { 9 | display: flex; 10 | flex-direction: column; 11 | padding: 0; 12 | } 13 | 14 | .datepicker-controls { 15 | display: flex; 16 | justify-content: space-between; 17 | width: 280px; 18 | margin: 0 auto; 19 | 20 | .selects-container { 21 | display: flex; 22 | } 23 | 24 | .select-wrapper { 25 | input { 26 | &:focus { 27 | border-bottom: none; 28 | } 29 | border-bottom: none; 30 | text-align: center; 31 | margin: 0; 32 | } 33 | 34 | .caret { 35 | display: none; 36 | } 37 | } 38 | 39 | .select-year input { 40 | width: 50px; 41 | } 42 | 43 | .select-month input { 44 | width: 70px; 45 | } 46 | } 47 | 48 | .month-prev, .month-next { 49 | margin-top: 4px; 50 | cursor: pointer; 51 | background-color: transparent; 52 | border: none; 53 | } 54 | 55 | 56 | /* Date Display */ 57 | .datepicker-date-display { 58 | flex: 1 auto; 59 | background-color: $secondary-color; 60 | color: #fff; 61 | padding: 20px 22px; 62 | font-weight: 500; 63 | 64 | .year-text { 65 | display: block; 66 | font-size: 1.5rem; 67 | line-height: 25px; 68 | color: $datepicker-year; 69 | } 70 | 71 | .date-text { 72 | display: block; 73 | font-size: 2.8rem; 74 | line-height: 47px; 75 | font-weight: 500; 76 | } 77 | } 78 | 79 | 80 | /* Calendar */ 81 | .datepicker-calendar-container { 82 | flex: 2.5 auto; 83 | } 84 | 85 | .datepicker-table { 86 | width: 280px; 87 | font-size: 1rem; 88 | margin: 0 auto; 89 | 90 | thead { 91 | border-bottom: none; 92 | } 93 | 94 | th { 95 | padding: 10px 5px; 96 | text-align: center; 97 | } 98 | 99 | tr { 100 | border: none; 101 | } 102 | 103 | abbr { 104 | text-decoration: none; 105 | color: $datepicker-calendar-header-color; 106 | } 107 | 108 | td { 109 | &.is-today { 110 | color: $secondary-color; 111 | } 112 | 113 | &.is-selected { 114 | background-color: $secondary-color; 115 | color: #fff; 116 | } 117 | 118 | &.is-outside-current-month, 119 | &.is-disabled { 120 | color: $datepicker-disabled-day-color; 121 | pointer-events: none; 122 | } 123 | 124 | border-radius: 50%; 125 | padding: 0; 126 | } 127 | } 128 | 129 | .datepicker-day-button { 130 | &:focus { 131 | background-color: $datepicker-day-focus; 132 | } 133 | 134 | background-color: transparent; 135 | border: none; 136 | line-height: 38px; 137 | display: block; 138 | width: 100%; 139 | border-radius: 50%; 140 | padding: 0 5px; 141 | cursor: pointer; 142 | color: inherit; 143 | } 144 | 145 | 146 | /* Footer */ 147 | .datepicker-footer { 148 | width: 280px; 149 | margin: 0 auto; 150 | padding-bottom: 5px; 151 | display: flex; 152 | justify-content: space-between; 153 | } 154 | 155 | .datepicker-cancel, 156 | .datepicker-clear, 157 | .datepicker-today, 158 | .datepicker-done { 159 | color: $secondary-color; 160 | padding: 0 1rem; 161 | } 162 | 163 | .datepicker-clear { 164 | color: $error-color; 165 | } 166 | 167 | 168 | /* Media Queries */ 169 | @media #{$medium-and-up} { 170 | .datepicker-modal { 171 | max-width: 625px; 172 | } 173 | 174 | .datepicker-container.modal-content { 175 | flex-direction: row; 176 | } 177 | 178 | .datepicker-date-display { 179 | flex: 0 1 270px; 180 | } 181 | 182 | .datepicker-controls, 183 | .datepicker-table, 184 | .datepicker-footer { 185 | width: 320px; 186 | } 187 | 188 | .datepicker-day-button { 189 | line-height: 44px; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_timepicker.scss: -------------------------------------------------------------------------------- 1 | /* Timepicker Containers */ 2 | .timepicker-modal { 3 | max-width: 325px; 4 | max-height: none; 5 | } 6 | 7 | .timepicker-container.modal-content { 8 | display: flex; 9 | flex-direction: column; 10 | padding: 0; 11 | } 12 | 13 | .text-primary { 14 | color: rgba(255, 255, 255, 1); 15 | } 16 | 17 | 18 | /* Clock Digital Display */ 19 | .timepicker-digital-display { 20 | flex: 1 auto; 21 | background-color: $secondary-color; 22 | padding: 10px; 23 | font-weight: 300; 24 | } 25 | 26 | .timepicker-text-container { 27 | font-size: 4rem; 28 | font-weight: bold; 29 | text-align: center; 30 | color: rgba(255, 255, 255, 0.6); 31 | font-weight: 400; 32 | position: relative; 33 | user-select: none; 34 | } 35 | 36 | .timepicker-span-hours, 37 | .timepicker-span-minutes, 38 | .timepicker-span-am-pm div { 39 | cursor: pointer; 40 | } 41 | 42 | .timepicker-span-hours { 43 | margin-right: 3px; 44 | } 45 | 46 | .timepicker-span-minutes { 47 | margin-left: 3px; 48 | } 49 | 50 | .timepicker-display-am-pm { 51 | font-size: 1.3rem; 52 | position: absolute; 53 | right: 1rem; 54 | bottom: 1rem; 55 | font-weight: 400; 56 | } 57 | 58 | 59 | /* Analog Clock Display */ 60 | .timepicker-analog-display { 61 | flex: 2.5 auto; 62 | } 63 | 64 | .timepicker-plate { 65 | background-color: $timepicker-clock-plate-bg; 66 | border-radius: 50%; 67 | width: 270px; 68 | height: 270px; 69 | overflow: visible; 70 | position: relative; 71 | margin: auto; 72 | margin-top: 25px; 73 | margin-bottom: 5px; 74 | user-select: none; 75 | } 76 | 77 | .timepicker-canvas, 78 | .timepicker-dial { 79 | position: absolute; 80 | left: 0; 81 | right: 0; 82 | top: 0; 83 | bottom: 0; 84 | } 85 | .timepicker-minutes { 86 | visibility: hidden; 87 | } 88 | 89 | .timepicker-tick { 90 | border-radius: 50%; 91 | color: $timepicker-clock-color; 92 | line-height: 40px; 93 | text-align: center; 94 | width: 40px; 95 | height: 40px; 96 | position: absolute; 97 | cursor: pointer; 98 | font-size: 15px; 99 | } 100 | 101 | .timepicker-tick.active, 102 | .timepicker-tick:hover { 103 | background-color: transparentize($secondary-color, .75); 104 | } 105 | .timepicker-dial { 106 | transition: transform 350ms, opacity 350ms; 107 | } 108 | .timepicker-dial-out { 109 | &.timepicker-hours { 110 | transform: scale(1.1, 1.1); 111 | } 112 | 113 | &.timepicker-minutes { 114 | transform: scale(.8, .8); 115 | } 116 | 117 | opacity: 0; 118 | } 119 | .timepicker-canvas { 120 | transition: opacity 175ms; 121 | 122 | line { 123 | stroke: $secondary-color; 124 | stroke-width: 4; 125 | stroke-linecap: round; 126 | } 127 | } 128 | .timepicker-canvas-out { 129 | opacity: 0.25; 130 | } 131 | .timepicker-canvas-bearing { 132 | stroke: none; 133 | fill: $secondary-color; 134 | } 135 | .timepicker-canvas-bg { 136 | stroke: none; 137 | fill: $secondary-color; 138 | } 139 | 140 | 141 | /* Footer */ 142 | .timepicker-footer { 143 | margin: 0 auto; 144 | padding: 5px 1rem; 145 | display: flex; 146 | justify-content: space-between; 147 | } 148 | 149 | .timepicker-clear { 150 | color: $error-color; 151 | } 152 | 153 | .timepicker-close { 154 | color: $secondary-color; 155 | } 156 | 157 | .timepicker-clear, 158 | .timepicker-close { 159 | padding: 0 20px; 160 | } 161 | 162 | /* Media Queries */ 163 | @media #{$medium-and-up} { 164 | .timepicker-modal { 165 | max-width: 600px; 166 | } 167 | 168 | .timepicker-container.modal-content { 169 | flex-direction: row; 170 | } 171 | 172 | .timepicker-text-container { 173 | top: 32%; 174 | } 175 | 176 | .timepicker-display-am-pm { 177 | position: relative; 178 | right: auto; 179 | bottom: auto; 180 | text-align: center; 181 | margin-top: 1.2rem; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_range.scss: -------------------------------------------------------------------------------- 1 | /* Range 2 | ========================================================================== */ 3 | 4 | .range-field { 5 | position: relative; 6 | } 7 | 8 | input[type=range], 9 | input[type=range] + .thumb { 10 | @extend .no-select; 11 | cursor: pointer; 12 | } 13 | 14 | input[type=range] { 15 | position: relative; 16 | background-color: transparent; 17 | border: none; 18 | outline: none; 19 | width: 100%; 20 | margin: 15px 0; 21 | padding: 0; 22 | 23 | &:focus { 24 | outline: none; 25 | } 26 | } 27 | 28 | input[type=range] + .thumb { 29 | position: absolute; 30 | top: 10px; 31 | left: 0; 32 | border: none; 33 | height: 0; 34 | width: 0; 35 | border-radius: 50%; 36 | background-color: $radio-fill-color; 37 | margin-left: 7px; 38 | 39 | transform-origin: 50% 50%; 40 | transform: rotate(-45deg); 41 | 42 | .value { 43 | display: block; 44 | width: 30px; 45 | text-align: center; 46 | color: $radio-fill-color; 47 | font-size: 0; 48 | transform: rotate(45deg); 49 | } 50 | 51 | &.active { 52 | border-radius: 50% 50% 50% 0; 53 | 54 | .value { 55 | color: $input-background; 56 | margin-left: -1px; 57 | margin-top: 8px; 58 | font-size: 10px; 59 | } 60 | } 61 | } 62 | 63 | // Shared 64 | @mixin range-track { 65 | height: $track-height; 66 | background: #c2c0c2; 67 | border: none; 68 | } 69 | 70 | @mixin range-thumb { 71 | border: none; 72 | height: $range-height; 73 | width: $range-width; 74 | border-radius: 50%; 75 | background: $radio-fill-color; 76 | transition: box-shadow .3s; 77 | } 78 | 79 | // WebKit 80 | input[type=range] { 81 | -webkit-appearance: none; 82 | } 83 | 84 | input[type=range]::-webkit-slider-runnable-track { 85 | @include range-track; 86 | } 87 | 88 | input[type=range]::-webkit-slider-thumb { 89 | @include range-thumb; 90 | -webkit-appearance: none; 91 | background-color: $radio-fill-color; 92 | transform-origin: 50% 50%; 93 | margin: -5px 0 0 0; 94 | 95 | } 96 | 97 | .keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb { 98 | box-shadow: 0 0 0 10px rgba($radio-fill-color, .26); 99 | } 100 | 101 | // FireFox 102 | input[type=range] { 103 | /* fix for FF unable to apply focus style bug */ 104 | border: 1px solid white; 105 | 106 | /*required for proper track sizing in FF*/ 107 | } 108 | 109 | input[type=range]::-moz-range-track { 110 | @include range-track; 111 | } 112 | 113 | input[type=range]::-moz-focus-inner { 114 | border: 0; 115 | } 116 | 117 | input[type=range]::-moz-range-thumb { 118 | @include range-thumb; 119 | margin-top: -5px; 120 | } 121 | 122 | // hide the outline behind the border 123 | input[type=range]:-moz-focusring { 124 | outline: 1px solid #fff; 125 | outline-offset: -1px; 126 | } 127 | 128 | .keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb { 129 | box-shadow: 0 0 0 10px rgba($radio-fill-color, .26); 130 | } 131 | 132 | // IE 10+ 133 | input[type=range]::-ms-track { 134 | height: $track-height; 135 | 136 | // remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead 137 | background: transparent; 138 | 139 | // leave room for the larger thumb to overflow with a transparent border */ 140 | border-color: transparent; 141 | border-width: 6px 0; 142 | 143 | /*remove default tick marks*/ 144 | color: transparent; 145 | } 146 | 147 | input[type=range]::-ms-fill-lower { 148 | background: #777; 149 | } 150 | 151 | input[type=range]::-ms-fill-upper { 152 | background: #ddd; 153 | } 154 | 155 | input[type=range]::-ms-thumb { 156 | @include range-thumb; 157 | } 158 | 159 | .keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb { 160 | box-shadow: 0 0 0 10px rgba($radio-fill-color, .26); 161 | } 162 | -------------------------------------------------------------------------------- /cc_lib/styles/django_forms.scss: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | 4 | $django-form-title-font-size: 1.4rem !default; 5 | $django-form-message-font-size: 0.9rem !default; 6 | $django-form-message-forecolor: white !default; 7 | $django-form-info-message-background-color: #4285F4 !default; 8 | $django-form-error-message-background-color: #F44259 !default; 9 | $django-form-button-foreground-color: white !default; 10 | $django-form-button-background-color: #7a9cd3 !default; 11 | $django-form-button-hover-background-color: #9fc1f8 !default; 12 | 13 | .generated-form { 14 | padding-top: 100px; 15 | align-self: center; 16 | height: 100%; 17 | min-width: 50%; 18 | 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-content: center; 23 | justify-items: center; 24 | 25 | position: relative; 26 | 27 | h1 { 28 | font-size: $django-form-title-font-size; 29 | margin-bottom: 20px; 30 | padding-bottom: 5px; 31 | width: 100%; 32 | border-bottom: 1px solid gray; 33 | } 34 | 35 | form { 36 | width: 100%; 37 | position: static; 38 | padding-left: 20px; 39 | 40 | p { 41 | display: block; 42 | position: relative; 43 | margin-bottom: 20px; 44 | 45 | } 46 | 47 | ul { 48 | display: none; 49 | } 50 | 51 | label { 52 | font-weight: bold; 53 | } 54 | 55 | input[type=text], input[type=password], input[type=email], select { 56 | margin-top: 5px; 57 | display: block; 58 | width: 50%; 59 | min-width: 300px; 60 | padding: 3px; 61 | } 62 | 63 | input[type=submit], button { 64 | border: none; 65 | background-color: $django-form-button-background-color; 66 | padding: 5px 20px; 67 | font-weight: bold; 68 | color: $django-form-button-foreground-color; 69 | cursor: pointer; 70 | 71 | &:hover { 72 | background-color: $django-form-button-hover-background-color; 73 | } 74 | } 75 | 76 | label + input[type=checkbox] { 77 | display: inline-block; 78 | margin: auto; 79 | margin-left: 10px; 80 | width: 1rem; 81 | height: 1rem; 82 | line-height: 200px; 83 | vertical-align: middle; 84 | } 85 | 86 | .errorlist ~ p + ul { 87 | display: block; 88 | padding-bottom: 20px; 89 | 90 | li { 91 | list-style: none; 92 | font-size: 0.8rem; 93 | } 94 | } 95 | 96 | .errorlist { 97 | display: inline-block; 98 | position: absolute; 99 | top: 0; 100 | padding: 0; 101 | width: 100%; 102 | 103 | li { 104 | @extend %message; 105 | list-style: none; 106 | background-color: $django-form-error-message-background-color; 107 | } 108 | } 109 | } 110 | } 111 | 112 | %message { 113 | width: 100%; 114 | margin-top: 5px; 115 | padding: 3px; 116 | align-content: center; 117 | text-align: center; 118 | font-weight: bold; 119 | color: white; 120 | font-size: $django-form-message-font-size; 121 | } 122 | 123 | .helptext { 124 | display: none; 125 | } 126 | 127 | .message-container { 128 | top: 0; 129 | padding: 0; 130 | width: 100%; 131 | 132 | .error-message, .error { 133 | @extend %message; 134 | background-color: $django-form-error-message-background-color; 135 | } 136 | 137 | .info-message, .info, .success { 138 | @extend %message; 139 | background-color: $django-form-info-message-background-color; 140 | } 141 | } -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_select.scss: -------------------------------------------------------------------------------- 1 | /* Select Field 2 | ========================================================================== */ 3 | 4 | select { display: none; } 5 | select.browser-default { display: block; } 6 | 7 | select { 8 | background-color: $select-background; 9 | width: 100%; 10 | padding: $select-padding; 11 | border: $select-border; 12 | border-radius: $select-radius; 13 | height: $input-height; 14 | } 15 | 16 | .select-label { 17 | position: absolute; 18 | } 19 | 20 | .select-wrapper { 21 | &.valid .helper-text[data-success], 22 | &.invalid ~ .helper-text[data-error] { 23 | @extend %hidden-text; 24 | } 25 | 26 | &.valid { 27 | & > input.select-dropdown { 28 | @extend %valid-input-style; 29 | } 30 | 31 | & ~ .helper-text:after { 32 | @extend %custom-success-message; 33 | } 34 | } 35 | 36 | &.invalid { 37 | & > input.select-dropdown, 38 | & > input.select-dropdown:focus { 39 | @extend %invalid-input-style; 40 | } 41 | 42 | & ~ .helper-text:after { 43 | @extend %custom-error-message; 44 | } 45 | } 46 | 47 | &.valid + label, 48 | &.invalid + label { 49 | width: 100%; 50 | pointer-events: none; 51 | } 52 | 53 | & + label:after { 54 | @extend %input-after-style; 55 | } 56 | 57 | position: relative; 58 | 59 | input.select-dropdown { 60 | &:focus { 61 | border-bottom: 1px solid $input-focus-color; 62 | } 63 | position: relative; 64 | cursor: pointer; 65 | background-color: transparent; 66 | border: none; 67 | border-bottom: $input-border; 68 | outline: none; 69 | height: $input-height; 70 | line-height: $input-height; 71 | width: 100%; 72 | font-size: $input-font-size; 73 | margin: $input-margin; 74 | padding: 0; 75 | display: block; 76 | user-select:none; 77 | z-index: 1; 78 | } 79 | 80 | .caret { 81 | position: absolute; 82 | right: 0; 83 | top: 0; 84 | bottom: 0; 85 | margin: auto 0; 86 | z-index: 0; 87 | fill: rgba(0,0,0,.87); 88 | } 89 | 90 | & + label { 91 | position: absolute; 92 | top: -26px; 93 | font-size: $label-font-size; 94 | } 95 | } 96 | 97 | // Disabled styles 98 | select:disabled { 99 | color: $input-disabled-color; 100 | } 101 | 102 | .select-wrapper.disabled { 103 | + label { 104 | color: $input-disabled-color; 105 | } 106 | .caret { 107 | fill: $input-disabled-color; 108 | } 109 | } 110 | 111 | .select-wrapper input.select-dropdown:disabled { 112 | color: $input-disabled-color; 113 | cursor: default; 114 | user-select: none; 115 | } 116 | 117 | .select-wrapper i { 118 | color: $select-disabled-color; 119 | } 120 | 121 | .select-dropdown li.disabled, 122 | .select-dropdown li.disabled > span, 123 | .select-dropdown li.optgroup { 124 | color: $select-disabled-color; 125 | background-color: transparent; 126 | } 127 | 128 | body.keyboard-focused { 129 | .select-dropdown.dropdown-content li:focus { 130 | background-color: $select-option-focus; 131 | } 132 | } 133 | 134 | .select-dropdown.dropdown-content { 135 | li { 136 | &:hover { 137 | background-color: $select-option-hover; 138 | } 139 | 140 | &.selected { 141 | background-color: $select-option-selected; 142 | } 143 | } 144 | } 145 | 146 | // Prefix Icons 147 | .prefix ~ .select-wrapper { 148 | margin-left: 3rem; 149 | width: 92%; 150 | width: calc(100% - 3rem); 151 | } 152 | 153 | .prefix ~ label { margin-left: 3rem; } 154 | 155 | // Icons 156 | .select-dropdown li { 157 | img { 158 | height: $dropdown-item-height - 10; 159 | width: $dropdown-item-height - 10; 160 | margin: 5px 15px; 161 | float: right; 162 | } 163 | } 164 | 165 | // Optgroup styles 166 | .select-dropdown li.optgroup { 167 | border-top: 1px solid $dropdown-hover-bg-color; 168 | 169 | &.selected > span { 170 | color: rgba(0, 0, 0, .7); 171 | } 172 | 173 | & > span { 174 | color: rgba(0, 0, 0, .4); 175 | } 176 | 177 | & ~ li.optgroup-option { 178 | padding-left: 1rem; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_cards.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .card-panel { 4 | transition: box-shadow .25s; 5 | padding: $card-padding; 6 | margin: $element-top-margin 0 $element-bottom-margin 0; 7 | border-radius: 2px; 8 | @extend .z-depth-1; 9 | background-color: $card-bg-color; 10 | } 11 | 12 | .card { 13 | position: relative; 14 | margin: $element-top-margin 0 $element-bottom-margin 0; 15 | background-color: $card-bg-color; 16 | transition: box-shadow .25s; 17 | border-radius: 2px; 18 | @extend .z-depth-1; 19 | 20 | 21 | .card-title { 22 | font-size: 24px; 23 | font-weight: 300; 24 | &.activator { 25 | cursor: pointer; 26 | } 27 | } 28 | 29 | // Card Sizes 30 | &.small, &.medium, &.large { 31 | position: relative; 32 | 33 | .card-image { 34 | max-height: 60%; 35 | overflow: hidden; 36 | } 37 | .card-image + .card-content { 38 | max-height: 40%; 39 | } 40 | .card-content { 41 | max-height: 100%; 42 | overflow: hidden; 43 | } 44 | .card-action { 45 | position: absolute; 46 | bottom: 0; 47 | left: 0; 48 | right: 0; 49 | } 50 | } 51 | 52 | &.small { 53 | height: 300px; 54 | } 55 | 56 | &.medium { 57 | height: 400px; 58 | } 59 | 60 | &.large { 61 | height: 500px; 62 | } 63 | 64 | // Horizontal Cards 65 | &.horizontal { 66 | &.small, &.medium, &.large { 67 | .card-image { 68 | height: 100%; 69 | max-height: none; 70 | overflow: visible; 71 | 72 | img { 73 | height: 100%; 74 | } 75 | } 76 | } 77 | 78 | display: flex; 79 | 80 | .card-image { 81 | max-width: 50%; 82 | img { 83 | border-radius: 2px 0 0 2px; 84 | max-width: 100%; 85 | width: auto; 86 | } 87 | } 88 | 89 | .card-stacked { 90 | display: flex; 91 | flex-direction: column; 92 | flex: 1; 93 | position: relative; 94 | 95 | .card-content { 96 | flex-grow: 1; 97 | } 98 | } 99 | } 100 | 101 | // Sticky Action Section 102 | &.sticky-action { 103 | .card-action { 104 | z-index: 2; 105 | } 106 | 107 | .card-reveal { 108 | z-index: 1; 109 | padding-bottom: 64px; 110 | } 111 | } 112 | 113 | 114 | 115 | 116 | .card-image { 117 | position: relative; 118 | 119 | // Image background for content 120 | img { 121 | display: block; 122 | border-radius: 2px 2px 0 0; 123 | position: relative; 124 | left: 0; 125 | right: 0; 126 | top: 0; 127 | bottom: 0; 128 | width: 100%; 129 | } 130 | 131 | .card-title { 132 | color: $card-bg-color; 133 | position: absolute; 134 | bottom: 0; 135 | left: 0; 136 | max-width: 100%; 137 | padding: $card-padding; 138 | } 139 | } 140 | 141 | .card-content { 142 | padding: $card-padding; 143 | border-radius: 0 0 2px 2px; 144 | 145 | p { 146 | margin: 0; 147 | } 148 | .card-title { 149 | display: block; 150 | line-height: 32px; 151 | margin-bottom: 8px; 152 | 153 | i { 154 | line-height: 32px; 155 | } 156 | } 157 | } 158 | 159 | .card-action { 160 | &:last-child { 161 | border-radius: 0 0 2px 2px; 162 | } 163 | background-color: inherit; // Use inherit to inherit color classes 164 | border-top: 1px solid rgba(160,160,160,.2); 165 | position: relative; 166 | padding: 16px $card-padding; 167 | 168 | a:not(.btn):not(.btn-large):not(.btn-floating) { 169 | color: $card-link-color; 170 | margin-right: $card-padding; 171 | transition: color .3s ease; 172 | text-transform: uppercase; 173 | 174 | &:hover { color: $card-link-color-light; } 175 | } 176 | } 177 | 178 | .card-reveal { 179 | padding: $card-padding; 180 | position: absolute; 181 | background-color: $card-bg-color; 182 | width: 100%; 183 | overflow-y: auto; 184 | left: 0; 185 | top: 100%; 186 | height: 100%; 187 | z-index: 3; 188 | display: none; 189 | 190 | .card-title { 191 | cursor: pointer; 192 | display: block; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /apps/fok/static/js/pushpin.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | let _defaults = { 5 | top: 0, 6 | bottom: Infinity, 7 | offset: 0, 8 | onPositionChange: null 9 | }; 10 | 11 | /** 12 | * @class 13 | * 14 | */ 15 | class Pushpin extends Component { 16 | /** 17 | * Construct Pushpin instance 18 | * @constructor 19 | * @param {Element} el 20 | * @param {Object} options 21 | */ 22 | constructor(el, options) { 23 | super(Pushpin, el, options); 24 | 25 | this.el.M_Pushpin = this; 26 | 27 | /** 28 | * Options for the modal 29 | * @member Pushpin#options 30 | */ 31 | this.options = $.extend({}, Pushpin.defaults, options); 32 | 33 | this.originalOffset = this.el.offsetTop; 34 | Pushpin._pushpins.push(this); 35 | this._setupEventHandlers(); 36 | this._updatePosition(); 37 | } 38 | 39 | static get defaults() { 40 | return _defaults; 41 | } 42 | 43 | static init(els, options) { 44 | return super.init(this, els, options); 45 | } 46 | 47 | /** 48 | * Get Instance 49 | */ 50 | static getInstance(el) { 51 | let domElem = !!el.jquery ? el[0] : el; 52 | return domElem.M_Pushpin; 53 | } 54 | 55 | /** 56 | * Teardown component 57 | */ 58 | destroy() { 59 | this.el.style.top = null; 60 | this._removePinClasses(); 61 | this._removeEventHandlers(); 62 | 63 | // Remove pushpin Inst 64 | let index = Pushpin._pushpins.indexOf(this); 65 | Pushpin._pushpins.splice(index, 1); 66 | } 67 | 68 | static _updateElements() { 69 | for (let elIndex in Pushpin._pushpins) { 70 | let pInstance = Pushpin._pushpins[elIndex]; 71 | pInstance._updatePosition(); 72 | } 73 | } 74 | 75 | _setupEventHandlers() { 76 | document.addEventListener('scroll', Pushpin._updateElements); 77 | } 78 | 79 | _removeEventHandlers() { 80 | document.removeEventListener('scroll', Pushpin._updateElements); 81 | } 82 | 83 | _updatePosition() { 84 | let scrolled = M.getDocumentScrollTop() + this.options.offset; 85 | 86 | if ( 87 | this.options.top <= scrolled && 88 | this.options.bottom >= scrolled && 89 | !this.el.classList.contains('pinned') 90 | ) { 91 | this._removePinClasses(); 92 | this.el.style.top = `${this.options.offset}px`; 93 | this.el.classList.add('pinned'); 94 | 95 | // onPositionChange callback 96 | if (typeof this.options.onPositionChange === 'function') { 97 | this.options.onPositionChange.call(this, 'pinned'); 98 | } 99 | } 100 | 101 | // Add pin-top (when scrolled position is above top) 102 | if (scrolled < this.options.top && !this.el.classList.contains('pin-top')) { 103 | this._removePinClasses(); 104 | this.el.style.top = 0; 105 | this.el.classList.add('pin-top'); 106 | 107 | // onPositionChange callback 108 | if (typeof this.options.onPositionChange === 'function') { 109 | this.options.onPositionChange.call(this, 'pin-top'); 110 | } 111 | } 112 | 113 | // Add pin-bottom (when scrolled position is below bottom) 114 | if (scrolled > this.options.bottom && !this.el.classList.contains('pin-bottom')) { 115 | this._removePinClasses(); 116 | this.el.classList.add('pin-bottom'); 117 | this.el.style.top = `${this.options.bottom - this.originalOffset}px`; 118 | 119 | // onPositionChange callback 120 | if (typeof this.options.onPositionChange === 'function') { 121 | this.options.onPositionChange.call(this, 'pin-bottom'); 122 | } 123 | } 124 | } 125 | 126 | _removePinClasses() { 127 | // IE 11 bug (can't remove multiple classes in one line) 128 | this.el.classList.remove('pin-top'); 129 | this.el.classList.remove('pinned'); 130 | this.el.classList.remove('pin-bottom'); 131 | } 132 | } 133 | 134 | /** 135 | * @static 136 | * @memberof Pushpin 137 | */ 138 | Pushpin._pushpins = []; 139 | 140 | M.Pushpin = Pushpin; 141 | 142 | if (M.jQueryLoaded) { 143 | M.initializeJqueryWrapper(Pushpin, 'pushpin', 'M_Pushpin'); 144 | } 145 | })(cash); 146 | -------------------------------------------------------------------------------- /apps/fok/static/js/parallax.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | let _defaults = { 5 | responsiveThreshold: 0 // breakpoint for swipeable 6 | }; 7 | 8 | class Parallax extends Component { 9 | constructor(el, options) { 10 | super(Parallax, el, options); 11 | 12 | this.el.M_Parallax = this; 13 | 14 | /** 15 | * Options for the Parallax 16 | * @member Parallax#options 17 | * @prop {Number} responsiveThreshold 18 | */ 19 | this.options = $.extend({}, Parallax.defaults, options); 20 | this._enabled = window.innerWidth > this.options.responsiveThreshold; 21 | 22 | this.$img = this.$el.find('img').first(); 23 | this.$img.each(function() { 24 | let el = this; 25 | if (el.complete) $(el).trigger('load'); 26 | }); 27 | 28 | this._updateParallax(); 29 | this._setupEventHandlers(); 30 | this._setupStyles(); 31 | 32 | Parallax._parallaxes.push(this); 33 | } 34 | 35 | static get defaults() { 36 | return _defaults; 37 | } 38 | 39 | static init(els, options) { 40 | return super.init(this, els, options); 41 | } 42 | 43 | /** 44 | * Get Instance 45 | */ 46 | static getInstance(el) { 47 | let domElem = !!el.jquery ? el[0] : el; 48 | return domElem.M_Parallax; 49 | } 50 | 51 | /** 52 | * Teardown component 53 | */ 54 | destroy() { 55 | Parallax._parallaxes.splice(Parallax._parallaxes.indexOf(this), 1); 56 | this.$img[0].style.transform = ''; 57 | this._removeEventHandlers(); 58 | 59 | this.$el[0].M_Parallax = undefined; 60 | } 61 | 62 | static _handleScroll() { 63 | for (let i = 0; i < Parallax._parallaxes.length; i++) { 64 | let parallaxInstance = Parallax._parallaxes[i]; 65 | parallaxInstance._updateParallax.call(parallaxInstance); 66 | } 67 | } 68 | 69 | static _handleWindowResize() { 70 | for (let i = 0; i < Parallax._parallaxes.length; i++) { 71 | let parallaxInstance = Parallax._parallaxes[i]; 72 | parallaxInstance._enabled = 73 | window.innerWidth > parallaxInstance.options.responsiveThreshold; 74 | } 75 | } 76 | 77 | _setupEventHandlers() { 78 | this._handleImageLoadBound = this._handleImageLoad.bind(this); 79 | this.$img[0].addEventListener('load', this._handleImageLoadBound); 80 | 81 | if (Parallax._parallaxes.length === 0) { 82 | Parallax._handleScrollThrottled = M.throttle(Parallax._handleScroll, 5); 83 | window.addEventListener('scroll', Parallax._handleScrollThrottled); 84 | 85 | Parallax._handleWindowResizeThrottled = M.throttle(Parallax._handleWindowResize, 5); 86 | window.addEventListener('resize', Parallax._handleWindowResizeThrottled); 87 | } 88 | } 89 | 90 | _removeEventHandlers() { 91 | this.$img[0].removeEventListener('load', this._handleImageLoadBound); 92 | 93 | if (Parallax._parallaxes.length === 0) { 94 | window.removeEventListener('scroll', Parallax._handleScrollThrottled); 95 | window.removeEventListener('resize', Parallax._handleWindowResizeThrottled); 96 | } 97 | } 98 | 99 | _setupStyles() { 100 | this.$img[0].style.opacity = 1; 101 | } 102 | 103 | _handleImageLoad() { 104 | this._updateParallax(); 105 | } 106 | 107 | _updateParallax() { 108 | let containerHeight = this.$el.height() > 0 ? this.el.parentNode.offsetHeight : 500; 109 | let imgHeight = this.$img[0].offsetHeight; 110 | let parallaxDist = imgHeight - containerHeight; 111 | let bottom = this.$el.offset().top + containerHeight; 112 | let top = this.$el.offset().top; 113 | let scrollTop = M.getDocumentScrollTop(); 114 | let windowHeight = window.innerHeight; 115 | let windowBottom = scrollTop + windowHeight; 116 | let percentScrolled = (windowBottom - top) / (containerHeight + windowHeight); 117 | let parallax = parallaxDist * percentScrolled; 118 | 119 | if (!this._enabled) { 120 | this.$img[0].style.transform = ''; 121 | } else if (bottom > scrollTop && top < scrollTop + windowHeight) { 122 | this.$img[0].style.transform = `translate3D(-50%, ${parallax}px, 0)`; 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * @static 129 | * @memberof Parallax 130 | */ 131 | Parallax._parallaxes = []; 132 | 133 | M.Parallax = Parallax; 134 | 135 | if (M.jQueryLoaded) { 136 | M.initializeJqueryWrapper(Parallax, 'parallax', 'M_Parallax'); 137 | } 138 | })(cash); 139 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_navbar.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | &.nav-extended { 3 | height: auto; 4 | 5 | .nav-wrapper { 6 | min-height: $navbar-height-mobile; 7 | height: auto; 8 | } 9 | 10 | .nav-content { 11 | position: relative; 12 | line-height: normal; 13 | } 14 | } 15 | 16 | color: $navbar-font-color; 17 | @extend .z-depth-1; 18 | background-color: $primary-color; 19 | width: 100%; 20 | height: $navbar-height-mobile; 21 | line-height: $navbar-line-height-mobile; 22 | 23 | a { color: $navbar-font-color; } 24 | 25 | i, 26 | [class^="mdi-"], [class*="mdi-"], 27 | i.material-icons { 28 | display: block; 29 | font-size: 24px; 30 | height: $navbar-height-mobile; 31 | line-height: $navbar-line-height-mobile; 32 | } 33 | 34 | .nav-wrapper { 35 | position: relative; 36 | height: 100%; 37 | } 38 | 39 | @media #{$large-and-up} { 40 | a.sidenav-trigger { display: none; } 41 | } 42 | 43 | 44 | // Collapse button 45 | .sidenav-trigger { 46 | float: left; 47 | position: relative; 48 | z-index: 1; 49 | height: $navbar-height-mobile; 50 | margin: 0 18px; 51 | 52 | i { 53 | height: $navbar-height-mobile; 54 | line-height: $navbar-line-height-mobile; 55 | } 56 | } 57 | 58 | 59 | // Logo 60 | .brand-logo { 61 | position: absolute; 62 | color: $navbar-font-color; 63 | display: inline-block; 64 | font-size: $navbar-brand-font-size; 65 | padding: 0; 66 | 67 | &.center { 68 | left: 50%; 69 | transform: translateX(-50%); 70 | } 71 | 72 | @media #{$medium-and-down} { 73 | left: 50%; 74 | transform: translateX(-50%); 75 | 76 | &.left, &.right { 77 | padding: 0; 78 | transform: none; 79 | } 80 | 81 | &.left { left: 0.5rem; } 82 | &.right { 83 | right: 0.5rem; 84 | left: auto; 85 | } 86 | } 87 | 88 | &.right { 89 | right: 0.5rem; 90 | padding: 0; 91 | } 92 | 93 | i, 94 | [class^="mdi-"], [class*="mdi-"], 95 | i.material-icons { 96 | float: left; 97 | margin-right: 15px; 98 | } 99 | } 100 | 101 | 102 | // Title 103 | .nav-title { 104 | display: inline-block; 105 | font-size: 32px; 106 | padding: 28px 0; 107 | } 108 | 109 | 110 | // Navbar Links 111 | ul { 112 | margin: 0; 113 | 114 | li { 115 | transition: background-color .3s; 116 | float: left; 117 | padding: 0; 118 | 119 | &.active { 120 | background-color: rgba(0,0,0,.1); 121 | } 122 | } 123 | a { 124 | transition: background-color .3s; 125 | font-size: $navbar-font-size; 126 | color: $navbar-font-color; 127 | display: block; 128 | padding: 0 15px; 129 | cursor: pointer; 130 | 131 | &.btn, &.btn-large, &.btn-flat, &.btn-floating { 132 | margin-top: -2px; 133 | margin-left: 15px; 134 | margin-right: 15px; 135 | 136 | & > .material-icons { 137 | height: inherit; 138 | line-height: inherit; 139 | } 140 | } 141 | 142 | &:hover { 143 | background-color: rgba(0,0,0,.1); 144 | } 145 | } 146 | 147 | &.left { 148 | float: left; 149 | } 150 | } 151 | 152 | // Navbar Search Form 153 | form { 154 | height: 100%; 155 | } 156 | 157 | .input-field { 158 | margin: 0; 159 | height: 100%; 160 | 161 | input { 162 | height: 100%; 163 | font-size: 1.2rem; 164 | border: none; 165 | padding-left: 2rem; 166 | 167 | &:focus, &[type=text]:valid, &[type=password]:valid, 168 | &[type=email]:valid, &[type=url]:valid, &[type=date]:valid { 169 | border: none; 170 | box-shadow: none; 171 | } 172 | } 173 | 174 | label { 175 | top: 0; 176 | left: 0; 177 | 178 | i { 179 | color: rgba(255,255,255,.7); 180 | transition: color .3s; 181 | } 182 | &.active i { color: $navbar-font-color; } 183 | } 184 | } 185 | } 186 | 187 | // Fixed Navbar 188 | .navbar-fixed { 189 | position: relative; 190 | height: $navbar-height-mobile; 191 | z-index: 997; 192 | 193 | nav { 194 | position: fixed; 195 | } 196 | } 197 | @media #{$medium-and-up} { 198 | nav.nav-extended .nav-wrapper { 199 | min-height: $navbar-height; 200 | } 201 | nav, nav .nav-wrapper i, nav a.sidenav-trigger, nav a.sidenav-trigger i { 202 | height: $navbar-height; 203 | line-height: $navbar-line-height; 204 | } 205 | .navbar-fixed { 206 | height: $navbar-height; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /apps/fok/templates/fok/pledge_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "generic.html" %} 2 | 3 | {% block generic %} 4 | 5 | {% load staticfiles %} 6 | {% load section %} 7 | 8 | 17 | 18 |
19 |
20 | 21 |

{{ object.title }}

22 |
23 |
24 |

Rationale

25 | {{ object.description | safe }} 26 |

Criteria

27 | {{ object.criteria | safe }} 28 |
29 |
30 | 31 |
32 |
33 |
34 | {% csrf_token %} 35 |
36 | {{ "pledge1"|dynamic_text }} 37 |
38 |
39 | 50 | 51 | 52 | 74 |
75 |
76 | Chosen value: {% if pledge.implication %}{{ pledge.implication }}{% else %}50{% endif %} 77 |
78 |
79 |
80 | {{ "pledge2"|dynamic_text }} 81 |
82 |
83 | {{ form.author_position }} 84 |
85 |
86 |
87 | {{ "pledge3"|dynamic_text }} 88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {% if pledge %} 96 | Update the pledge 97 | {% else %} 98 | Sign the pledge 99 | {% endif %} 100 |
101 |
102 | {% endblock %} 103 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_sidenav.scss: -------------------------------------------------------------------------------- 1 | .sidenav { 2 | position: fixed; 3 | width: $sidenav-width; 4 | left: 0; 5 | top: 0; 6 | margin: 0; 7 | transform: translateX(-100%); 8 | height: 100%; 9 | height: calc(100% + 60px); 10 | height: -moz-calc(100%); //Temporary Firefox Fix 11 | padding-bottom: 60px; 12 | background-color: $sidenav-bg-color; 13 | z-index: 999; 14 | overflow-y: auto; 15 | will-change: transform; 16 | backface-visibility: hidden; 17 | transform: translateX(-105%); 18 | 19 | @extend .z-depth-1; 20 | 21 | // Right Align 22 | &.right-aligned { 23 | right: 0; 24 | transform: translateX(105%); 25 | left: auto; 26 | transform: translateX(100%); 27 | } 28 | 29 | .collapsible { 30 | margin: 0; 31 | } 32 | 33 | 34 | li { 35 | float: none; 36 | line-height: $sidenav-line-height; 37 | 38 | &.active { background-color: rgba(0,0,0,.05); } 39 | } 40 | 41 | li > a { 42 | color: $sidenav-font-color; 43 | display: block; 44 | font-size: $sidenav-font-size; 45 | font-weight: 500; 46 | height: $sidenav-item-height; 47 | line-height: $sidenav-line-height; 48 | padding: 0 ($sidenav-padding * 2); 49 | 50 | &:hover { background-color: rgba(0,0,0,.05);} 51 | 52 | &.btn, &.btn-large, &.btn-flat, &.btn-floating { 53 | margin: 10px 15px; 54 | } 55 | 56 | &.btn, 57 | &.btn-large, 58 | &.btn-floating { color: $button-raised-color; } 59 | &.btn-flat { color: $button-flat-color; } 60 | 61 | &.btn:hover, 62 | &.btn-large:hover { background-color: lighten($button-raised-background, 5%); } 63 | &.btn-floating:hover { background-color: $button-raised-background; } 64 | 65 | & > i, 66 | & > [class^="mdi-"], li > a > [class*="mdi-"], 67 | & > i.material-icons { 68 | float: left; 69 | height: $sidenav-item-height; 70 | line-height: $sidenav-line-height; 71 | margin: 0 ($sidenav-padding * 2) 0 0; 72 | width: $sidenav-item-height / 2; 73 | color: rgba(0,0,0,.54); 74 | } 75 | } 76 | 77 | 78 | .divider { 79 | margin: ($sidenav-padding / 2) 0 0 0; 80 | } 81 | 82 | .subheader { 83 | &:hover { 84 | background-color: transparent; 85 | } 86 | 87 | cursor: initial; 88 | pointer-events: none; 89 | color: rgba(0,0,0,.54); 90 | font-size: $sidenav-font-size; 91 | font-weight: 500; 92 | line-height: $sidenav-line-height; 93 | } 94 | 95 | .user-view { 96 | position: relative; 97 | padding: ($sidenav-padding * 2) ($sidenav-padding * 2) 0; 98 | margin-bottom: $sidenav-padding / 2; 99 | 100 | & > a { 101 | &:hover { background-color: transparent; } 102 | height: auto; 103 | padding: 0; 104 | } 105 | 106 | .background { 107 | overflow: hidden; 108 | position: absolute; 109 | top: 0; 110 | right: 0; 111 | bottom: 0; 112 | left: 0; 113 | z-index: -1; 114 | } 115 | 116 | .circle, .name, .email { 117 | display: block; 118 | } 119 | 120 | .circle { 121 | height: 64px; 122 | width: 64px; 123 | } 124 | 125 | .name, 126 | .email { 127 | font-size: $sidenav-font-size; 128 | line-height: $sidenav-line-height / 2; 129 | } 130 | 131 | .name { 132 | margin-top: 16px; 133 | font-weight: 500; 134 | } 135 | 136 | .email { 137 | padding-bottom: 16px; 138 | font-weight: 400; 139 | } 140 | } 141 | } 142 | 143 | 144 | // Touch interaction 145 | .drag-target { 146 | // Right Align 147 | &.right-aligned { 148 | right: 0; 149 | } 150 | 151 | height: 100%; 152 | width: 10px; 153 | position: fixed; 154 | top: 0; 155 | z-index: 998; 156 | } 157 | 158 | 159 | // Fixed Sidenav shown 160 | .sidenav.sidenav-fixed { 161 | // Right Align 162 | &.right-aligned { 163 | right: 0; 164 | left: auto; 165 | } 166 | 167 | left: 0; 168 | transform: translateX(0); 169 | position: fixed; 170 | } 171 | 172 | // Fixed Sidenav hide on smaller 173 | @media #{$medium-and-down} { 174 | .sidenav { 175 | &.sidenav-fixed { 176 | transform: translateX(-105%); 177 | 178 | &.right-aligned { 179 | transform: translateX(105%); 180 | } 181 | } 182 | 183 | > a { 184 | padding: 0 $sidenav-padding; 185 | } 186 | 187 | .user-view { 188 | padding: $sidenav-padding $sidenav-padding 0; 189 | } 190 | } 191 | } 192 | 193 | 194 | .sidenav .collapsible-body > ul:not(.collapsible) > li.active, 195 | .sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active { 196 | background-color: $primary-color; 197 | a { 198 | color: $sidenav-bg-color; 199 | } 200 | } 201 | .sidenav .collapsible-body { 202 | padding: 0; 203 | } 204 | 205 | 206 | .sidenav-overlay { 207 | position: fixed; 208 | top: 0; 209 | left: 0; 210 | right: 0; 211 | opacity: 0; 212 | height: 120vh; 213 | background-color: rgba(0,0,0,.5); 214 | z-index: 997; 215 | display: none; 216 | } 217 | -------------------------------------------------------------------------------- /apps/fok/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | from uuid import uuid4 4 | from django.contrib.auth import get_user_model 5 | from cc_lib.utils import implement_slug 6 | from django.shortcuts import reverse 7 | from django.contrib.auth.models import UserManager 8 | from constance import config as cons 9 | 10 | 11 | def upload_path(instance, filename): 12 | if isinstance(instance, Campaign): 13 | return 'campaign.image/{0}.png'.format(str(uuid4()), filename) 14 | 15 | 16 | class CCUserManager(UserManager): 17 | def create_superuser(self, email, password, **extra_fields): 18 | return super().create_superuser(email, email, password, **extra_fields) 19 | 20 | 21 | class Background(models.Model): 22 | name = models.CharField(max_length=100) 23 | 24 | def __str__(self): 25 | return self.name 26 | 27 | 28 | class User(AbstractUser): 29 | REQUIRED_FIELDS = [] 30 | USERNAME_FIELD = 'id' 31 | objects = CCUserManager() 32 | username = models.CharField(max_length=30, unique=False) 33 | password = models.CharField(max_length=120, unique=False, blank=True, null=True) 34 | name = models.CharField(max_length=150, unique=False) 35 | background = models.ForeignKey( 36 | Background, 37 | null=True, 38 | blank=True, 39 | on_delete=models.SET_NULL, 40 | verbose_name='Research Field' 41 | ) 42 | newsletter = models.BooleanField(default=False) 43 | is_greeted = models.BooleanField(default=False) 44 | 45 | @property 46 | def pledged_campaigns(self): 47 | return [pledge.campaign for pledge in self.pledges.all()] 48 | 49 | def __str__(self): 50 | if self.first_name and self.last_name: 51 | return f'{self.first_name} {self.last_name}' 52 | return self.name 53 | 54 | 55 | class Campaign(models.Model): 56 | title = models.CharField(max_length=500) 57 | slug = models.CharField(max_length=500, editable=False) 58 | image = models.ImageField(null=True, upload_to=upload_path) 59 | short_description = models.CharField(max_length=500, null=True, blank=True) 60 | description = models.TextField() 61 | criteria = models.TextField() 62 | visible = models.BooleanField(default=False) 63 | created_at = models.DateTimeField(auto_now=True, editable=False) 64 | position = models.IntegerField(default=0) 65 | 66 | @property 67 | def absolute_url(self): 68 | return reverse('campaign', args=[str(self.slug)]) 69 | 70 | def get_pledge_from(self, user): 71 | return next(iter([pledge for pledge in self.pledges.all() if pledge.user == user]), None) 72 | 73 | @property 74 | def pledge_url(self): 75 | return reverse('pledge', args=[str(self.slug)]) 76 | 77 | @property 78 | def stats(self): 79 | from collections import Counter 80 | from constance import config 81 | 82 | pledges = self.pledges.all() 83 | fields = [pledge.user.background for pledge in pledges] 84 | public_users = [str(pledge.user) for pledge in pledges if pledge.allow_public_name] 85 | 86 | n_pledges = len(pledges) 87 | n_public_users = len(public_users) 88 | n_anonymous_users = n_pledges - n_public_users 89 | n_fields = len(fields) 90 | 91 | fields_percentage = {c[0]: int(c[1]/n_fields * 100) for c in Counter(fields).most_common()} 92 | anonymous_pledges_percentage = (n_anonymous_users / n_pledges) * 100 \ 93 | if n_anonymous_users != 0 \ 94 | else 0 95 | 96 | support_metric = int((n_public_users + (n_anonymous_users * config.ANONYMOUS_USERS_FACTOR)) / n_pledges * 100) \ 97 | if n_pledges != 0 else 0 98 | 99 | return { 100 | 'public_user_pledges': public_users, 101 | 'pledges_count': n_pledges, 102 | 'research_field_impact': fields_percentage, 103 | 'anonymous_pledges_count': n_anonymous_users, 104 | 'anonymous_pledges_percentage': f'{anonymous_pledges_percentage:.2f}', 105 | 'support_metric': f'{support_metric:.2f}' if not cons.OVERRIDE_SUPPORT_METRICS_TEXT else cons.OVERRIDE_SUPPORT_METRICS_TEXT 106 | } 107 | 108 | def __str__(self): 109 | return self.title 110 | 111 | 112 | implement_slug(Campaign, 'title') 113 | 114 | 115 | class EnabledAuthorPosition(models.Model): 116 | position = models.CharField(max_length=25) 117 | 118 | def __str__(self): 119 | return self.position 120 | 121 | 122 | class Pledge(models.Model): 123 | class Meta: 124 | ordering = ['created_at'] 125 | 126 | user = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, null=True, related_name='pledges') 127 | campaign = models.ForeignKey(Campaign, on_delete=models.SET_NULL, null=True, related_name='pledges') 128 | implication = models.FloatField(verbose_name='Threshold', blank=True, default=None) 129 | author_position = models.ManyToManyField(EnabledAuthorPosition, blank=True, default=None) 130 | allow_public_name = models.BooleanField(default=False) 131 | created_at = models.DateTimeField(auto_now_add=True) 132 | updated_at = models.DateTimeField(auto_now=True) 133 | 134 | def __str__(self): 135 | return f'{str(self.user)} ({self.user.email}) pledge to {self.campaign.title}' 136 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/forms/_checkboxes.scss: -------------------------------------------------------------------------------- 1 | /* Checkboxes 2 | ========================================================================== */ 3 | 4 | /* Remove default checkbox */ 5 | [type="checkbox"]:not(:checked), 6 | [type="checkbox"]:checked { 7 | position: absolute; 8 | opacity: 0; 9 | pointer-events: none; 10 | } 11 | 12 | // Checkbox Styles 13 | [type="checkbox"] { 14 | // Text Label Style 15 | + span:not(.lever) { 16 | position: relative; 17 | padding-left: 35px; 18 | cursor: pointer; 19 | display: inline-block; 20 | height: 25px; 21 | line-height: 25px; 22 | font-size: 1rem; 23 | user-select: none; 24 | } 25 | 26 | /* checkbox aspect */ 27 | + span:not(.lever):before, 28 | &:not(.filled-in) + span:not(.lever):after { 29 | content: ''; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | width: 18px; 34 | height: 18px; 35 | z-index: 0; 36 | border: 2px solid $radio-empty-color; 37 | border-radius: 1px; 38 | margin-top: 3px; 39 | transition: .2s; 40 | } 41 | 42 | &:not(.filled-in) + span:not(.lever):after { 43 | border: 0; 44 | transform: scale(0); 45 | } 46 | 47 | &:not(:checked):disabled + span:not(.lever):before { 48 | border: none; 49 | background-color: $input-disabled-color; 50 | } 51 | 52 | // Focused styles 53 | &.tabbed:focus + span:not(.lever):after { 54 | transform: scale(1); 55 | border: 0; 56 | border-radius: 50%; 57 | box-shadow: 0 0 0 10px rgba(0,0,0,.1); 58 | background-color: rgba(0,0,0,.1); 59 | } 60 | } 61 | 62 | [type="checkbox"]:checked { 63 | + span:not(.lever):before { 64 | top: -4px; 65 | left: -5px; 66 | width: 12px; 67 | height: 22px; 68 | border-top: 2px solid transparent; 69 | border-left: 2px solid transparent; 70 | border-right: $radio-border; 71 | border-bottom: $radio-border; 72 | transform: rotate(40deg); 73 | backface-visibility: hidden; 74 | transform-origin: 100% 100%; 75 | } 76 | 77 | &:disabled + span:before { 78 | border-right: 2px solid $input-disabled-color; 79 | border-bottom: 2px solid $input-disabled-color; 80 | } 81 | } 82 | 83 | /* Indeterminate checkbox */ 84 | [type="checkbox"]:indeterminate { 85 | + span:not(.lever):before { 86 | top: -11px; 87 | left: -12px; 88 | width: 10px; 89 | height: 22px; 90 | border-top: none; 91 | border-left: none; 92 | border-right: $radio-border; 93 | border-bottom: none; 94 | transform: rotate(90deg); 95 | backface-visibility: hidden; 96 | transform-origin: 100% 100%; 97 | } 98 | 99 | // Disabled indeterminate 100 | &:disabled + span:not(.lever):before { 101 | border-right: 2px solid $input-disabled-color; 102 | background-color: transparent; 103 | } 104 | } 105 | 106 | // Filled in Style 107 | [type="checkbox"].filled-in { 108 | // General 109 | + span:not(.lever):after { 110 | border-radius: 2px; 111 | } 112 | 113 | + span:not(.lever):before, 114 | + span:not(.lever):after { 115 | content: ''; 116 | left: 0; 117 | position: absolute; 118 | /* .1s delay is for check animation */ 119 | transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s; 120 | z-index: 1; 121 | } 122 | 123 | // Unchecked style 124 | &:not(:checked) + span:not(.lever):before { 125 | width: 0; 126 | height: 0; 127 | border: 3px solid transparent; 128 | left: 6px; 129 | top: 10px; 130 | transform: rotateZ(37deg); 131 | transform-origin: 100% 100%; 132 | } 133 | 134 | &:not(:checked) + span:not(.lever):after { 135 | height: 20px; 136 | width: 20px; 137 | background-color: transparent; 138 | border: 2px solid $radio-empty-color; 139 | top: 0px; 140 | z-index: 0; 141 | } 142 | 143 | // Checked style 144 | &:checked { 145 | + span:not(.lever):before { 146 | top: 0; 147 | left: 1px; 148 | width: 8px; 149 | height: 13px; 150 | border-top: 2px solid transparent; 151 | border-left: 2px solid transparent; 152 | border-right: 2px solid $input-background; 153 | border-bottom: 2px solid $input-background; 154 | transform: rotateZ(37deg); 155 | transform-origin: 100% 100%; 156 | } 157 | 158 | + span:not(.lever):after { 159 | top: 0; 160 | width: 20px; 161 | height: 20px; 162 | border: 2px solid $secondary-color; 163 | background-color: $secondary-color; 164 | z-index: 0; 165 | } 166 | } 167 | 168 | // Focused styles 169 | &.tabbed:focus + span:not(.lever):after { 170 | border-radius: 2px; 171 | border-color: $radio-empty-color; 172 | background-color: rgba(0,0,0,.1); 173 | } 174 | 175 | &.tabbed:checked:focus + span:not(.lever):after { 176 | border-radius: 2px; 177 | background-color: $secondary-color; 178 | border-color: $secondary-color; 179 | } 180 | 181 | // Disabled style 182 | &:disabled:not(:checked) + span:not(.lever):before { 183 | background-color: transparent; 184 | border: 2px solid transparent; 185 | } 186 | 187 | &:disabled:not(:checked) + span:not(.lever):after { 188 | border-color: transparent; 189 | background-color: $input-disabled-solid-color; 190 | } 191 | 192 | &:disabled:checked + span:not(.lever):before { 193 | background-color: transparent; 194 | } 195 | 196 | &:disabled:checked + span:not(.lever):after { 197 | background-color: $input-disabled-solid-color; 198 | border-color: $input-disabled-solid-color; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /apps/fok/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-07-06 07:24 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import fok.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('auth', '0009_alter_user_last_name_max_length'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='User', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('password', models.CharField(max_length=128, verbose_name='password')), 24 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 25 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('username', models.CharField(max_length=20)), 33 | ('name', models.CharField(max_length=150)), 34 | ('newsletter', models.BooleanField(default=False)), 35 | ], 36 | options={ 37 | 'verbose_name': 'user', 38 | 'verbose_name_plural': 'users', 39 | 'abstract': False, 40 | }, 41 | managers=[ 42 | ('objects', fok.models.CCUserManager()), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='Background', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('name', models.CharField(max_length=100)), 50 | ], 51 | ), 52 | migrations.CreateModel( 53 | name='Campaign', 54 | fields=[ 55 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 56 | ('title', models.CharField(max_length=500)), 57 | ('slug', models.CharField(editable=False, max_length=500)), 58 | ('image', models.ImageField(null=True, upload_to=fok.models.upload_path)), 59 | ('short_description', models.CharField(blank=True, max_length=500, null=True)), 60 | ('description', models.TextField()), 61 | ('criteria', models.TextField()), 62 | ('visible', models.BooleanField(default=False)), 63 | ('created_at', models.DateTimeField(auto_now=True)), 64 | ('position', models.IntegerField(default=0)), 65 | ], 66 | ), 67 | migrations.CreateModel( 68 | name='EnabledAuthorPosition', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('position', models.CharField(max_length=25)), 72 | ], 73 | ), 74 | migrations.CreateModel( 75 | name='Pledge', 76 | fields=[ 77 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 78 | ('implication', models.FloatField(verbose_name='Threshold')), 79 | ('allow_public_name', models.BooleanField(default=False)), 80 | ('created_at', models.DateTimeField(auto_now_add=True)), 81 | ('updated_at', models.DateTimeField(auto_now=True)), 82 | ('author_position', models.ManyToManyField(to='fok.EnabledAuthorPosition')), 83 | ('campaign', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pledges', to='fok.Campaign')), 84 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pledges', to=settings.AUTH_USER_MODEL)), 85 | ], 86 | options={ 87 | 'ordering': ['created_at'], 88 | }, 89 | ), 90 | migrations.AddField( 91 | model_name='user', 92 | name='background', 93 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='fok.Background', verbose_name='Research Field'), 94 | ), 95 | migrations.AddField( 96 | model_name='user', 97 | name='groups', 98 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), 99 | ), 100 | migrations.AddField( 101 | model_name='user', 102 | name='user_permissions', 103 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), 104 | ), 105 | ] 106 | -------------------------------------------------------------------------------- /apps/fok/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load staticfiles %} 4 | {% load section %} 5 | 6 | 7 | 8 | Free our knowledge 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if messages %} 19 | {% include 'fok/partials/messages.html' %} 20 | {% endif %} 21 | {% if not user.is_anonymous and not user.is_greeted %} 22 | 23 | 66 | 75 | {% endif %} 76 |
77 |
78 |
79 | 80 | 81 | 82 |
83 | {% if user.is_anonymous %} 84 |
85 | 92 |
93 | {% else %} 94 |
95 | 103 |
104 | {% endif %} 105 | 106 |
107 |
110 |
111 |
112 |
113 |
114 | {% block content %} 115 | {% endblock %} 116 |
117 |
118 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /cc_lib/commands/generate_fakes_command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # From: https://docs.djangoproject.com/en/2.1/howto/custom-management-commands/ 5 | 6 | from django.core.management.base import BaseCommand 7 | from django.core.management.commands.flush import Command as Flush 8 | from django.db import DEFAULT_DB_ALIAS 9 | from django.conf import settings 10 | from cc_lib.utils import get_class_from_route 11 | import inspect 12 | import random 13 | import factory 14 | from factory import fuzzy 15 | 16 | 17 | relationship_strategies = { 18 | 'choice': (lambda **kwargs: fuzzy.FuzzyChoice(kwargs['objects']), False), 19 | 'sample': (lambda **kwargs: random.sample(kwargs['objects'], kwargs['number']), True), 20 | 'iterate': (lambda **kwargs: factory.Iterator(kwargs['objects']), False) 21 | } 22 | 23 | 24 | class GenerateFakesCommand(BaseCommand): 25 | help = 'Generates fake data for all the models, for testing purposes.' 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | self.factories_dict = {} 30 | self.factory_by_models = {} 31 | 32 | def add_arguments(self, parser): 33 | parser.add_argument( 34 | '--flush', '--flush', action='store_true', dest='flush' 35 | ) 36 | 37 | def generated_fake_data(self, objects, model, factory_obj): 38 | pass 39 | 40 | def fakes_generation_finished(self): 41 | pass 42 | 43 | def get_image_fields(self, factory_class): 44 | from django.db.models.fields.files import ImageFileDescriptor 45 | return [f[0] for f in inspect.getmembers(factory_class._meta.model) if isinstance(f[1], ImageFileDescriptor) and f[0] in factory_class._meta.declarations.keys()] 46 | 47 | def get_related_data(self, factory_cls, related): 48 | related_instances = {} 49 | lazy_related = {} 50 | if related: 51 | for field_name, values in related.items(): 52 | relationship_field = getattr(factory_cls._meta.model, field_name) 53 | to_class = relationship_field.field.related_model._meta.label.split('.')[-1] 54 | assert to_class in self.factory_by_models.keys(), \ 55 | f""" 56 | You have to generate {to_class} fixtures, before {cls_name} fixtures for assigning them 57 | to the {field_name} field. 58 | """ 59 | related_objects = self.get_generated_objects(to_class) 60 | strategy = values.get('strategy', 'iterate') 61 | strategy, many = relationship_strategies[strategy] 62 | dest = lazy_related if many else related_instances 63 | dest[field_name] = strategy(objects=related_objects, **values) 64 | return related_instances, lazy_related 65 | 66 | def create_data(self, factory_cls, number=50, related=None, fields_value=None, field_values=None, extra_objects=None): 67 | """ 68 | 69 | :param factory_cls: 70 | :param number: 71 | :param related: 72 | :param fields_value: It is an object with the same property name than the fixture. It will set that value on the created fixtures. 73 | :param field_values: It is a list of values for field names. It will iterate over those values and set them on the created fixtures. 74 | :param extra_objects: 75 | :return: 76 | """ 77 | cls_name = factory_cls._meta.model.__name__ 78 | related_instances, lazy_related = self.get_related_data(factory_cls, related) 79 | field_values_to_assign = {} if field_values is None else \ 80 | {field_name: factory.Iterator(values) for field_name, values in field_values.items()} 81 | fields_value = {} if not fields_value else fields_value 82 | objects = factory_cls.create_batch( 83 | size=number, 84 | **fields_value, 85 | **field_values_to_assign, 86 | **related_instances 87 | ) 88 | for field_name, related_objs in lazy_related.items(): 89 | for obj in objects: 90 | collection = getattr(obj, field_name) 91 | collection.set(related_objs) 92 | obj.save() 93 | self.stdout.write(self.style.SUCCESS(f'Fake data ({number} objects) for {cls_name} model created.')) 94 | self.generated_fake_data(objects, cls_name, factory_cls) 95 | 96 | if extra_objects: 97 | for data in extra_objects: 98 | objects = objects + self.create_data(factory_cls, **data) 99 | 100 | return objects 101 | 102 | def download_and_upload_images(self, objects, fields): 103 | import urllib.request as request 104 | import tempfile 105 | from django.core.files import File 106 | 107 | def _download_and_upload_images(obj, prop): 108 | if getattr(obj, prop) is None: 109 | return 110 | url = str(getattr(obj, prop)) 111 | response = request.urlopen(url) 112 | data = response.read() 113 | fp = tempfile.TemporaryFile() 114 | fp.write(data) 115 | fp.seek(0) 116 | setattr(obj, prop, File(fp)) 117 | obj.save() 118 | 119 | for field in fields: 120 | [_download_and_upload_images(obj, field) for obj in objects] 121 | len(objects) > 0 and self.stdout.write( 122 | self.style.SUCCESS(f'Updated {field} field image for model {str(objects[0].__class__.__name__)}.') 123 | ) 124 | 125 | def get_generated_objects(self, cls): 126 | related_factory = self.factory_by_models[cls] 127 | return self.factories_dict[related_factory]['objects'] 128 | 129 | def get_from_yml(self, path): 130 | import yaml 131 | with open(path) as ymlfile: 132 | yml_string = ymlfile.read() 133 | return yaml.load(yml_string) 134 | 135 | def handle(self, *args, **options): 136 | should_ask = not options['flush'] 137 | Flush().handle(interactive=should_ask, database=DEFAULT_DB_ALIAS, **options) 138 | assert hasattr(settings, 'FIXTURE_FACTORIES'), """ 139 | You should define FIXTURE_FACTORIES list into the settings file before creating fixtures. 140 | """ 141 | factories = settings.FIXTURE_FACTORIES \ 142 | if isinstance(settings.FIXTURE_FACTORIES, dict) \ 143 | else self.get_from_yml(settings.FIXTURE_FACTORIES) 144 | self.factories_dict = {} 145 | self.factory_by_models = {} 146 | have_images_list = [] 147 | for factory_class_route, factory_obj in factories.items(): 148 | factory_class = get_class_from_route(factory_class_route) 149 | cls_name = factory_class._meta.model.__name__ 150 | fnc = getattr(self, 'create_' + cls_name.lower() + 's', self.create_data) 151 | factory_obj = factory_obj if factory_obj is not None else {} 152 | objects = fnc(factory_class, **factory_obj) 153 | self.factory_by_models[cls_name] = factory_class_route 154 | self.factories_dict[factory_class_route] = { 155 | 'objects': objects, 156 | 'factory': factory_class 157 | } 158 | image_fields = self.get_image_fields(factory_class) 159 | len(image_fields) > 0 and have_images_list.append((objects, image_fields)) 160 | [self.download_and_upload_images(*elements_with_images) for elements_with_images in have_images_list] 161 | self.fakes_generation_finished() 162 | -------------------------------------------------------------------------------- /free_our_knowledge/settings/dev.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for free_our_knowledge project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, '../apps'))) 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'gd7^%u6zb3%nb%p^_+^4)f1qom2%8l)32#9n#*m$=jd1ndg_zz' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'django_summernote', 42 | 'fok', 43 | 'constance.backends.database', 44 | 'constance', 45 | 'cc_cms' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'free_our_knowledge.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | 'fok.context_processors.add_config' 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'free_our_knowledge.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.postgresql', 86 | 'NAME': 'fok', 87 | 'USER': 'postgres', 88 | 'PASSWORD': 'mysecretpassword', 89 | 'HOST': 'docker_db_1', #os.environ.get('DB_HOST', '127.0.0.1') 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | AUTHENTICATION_BACKENDS = [ 127 | # 'django.contrib.auth.backends.ModelBackend' 128 | 'fok.backends.OrcidBackend' 129 | ] 130 | 131 | USE_ORCID = False 132 | BASE_ORCID_URL = None 133 | ORCID_ID = None 134 | ORCID_SECRET = None 135 | ORCID_REDIRECT_URL = None 136 | BASE_ORCID_API_URL = None 137 | 138 | # Static files (CSS, JavaScript, Images) 139 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 140 | 141 | STATIC_URL = '/static/' 142 | STATIC_ROOT = 'static' 143 | LOGIN_REDIRECT_URL = '/' 144 | 145 | DEV_SETTINGS_MODULE = 'free_our_knowledge.settings.dev' 146 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 147 | AUTH_USER_MODEL = 'fok.User' 148 | SIGNUP_FORM = 'fok.forms.FokSignUpForm' 149 | 150 | FIXTURE_FACTORIES = 'free_our_knowledge/resources/fixtures.yml' 151 | 152 | CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' 153 | CONSTANCE_CONFIG = { 154 | 'OVERRIDE_SUPPORT_METRICS_TEXT': ("", """ 155 | If this text value is not empty, when a display metric is gonna be shown will be replaced by this text. 156 | """ 157 | ), 158 | 'ANONYMOUS_USERS_FACTOR': (0.25, """ 159 | The factor to estimate the impact into the support metric for non registered users. 160 | """), 161 | 'FRONTPAGE_SENTENCE': ( 162 | 'People empowering the future of scholarly communication', 163 | 'The sentence that appears over the image into the front page. ' 164 | ), 165 | 'FRONTPAGE_VIDEO_URL': ( 166 | 'https://s3.wasabisys.com/codi.coop/fok-mvp/FreeOurKnowledge_linked2website.mp4', 167 | 'Video url for the front page.' 168 | ), 169 | 'CAMPAIGNS_TITLE_IN_FRONTPAGE': ( 170 | 'Together we can fix academia', 171 | 'Text shown as title on the frontpage for campaigns section.' 172 | ), 173 | 'INTRODUCTION_TITLE_IN_FRONTPAGE': ( 174 | 'Scholarly publishing is broken', 175 | 'Text shown as title on the frontpage for the introduction section.' 176 | ), 177 | 'ACTIVATE_GREETINGS': ( 178 | True, 179 | 'Activate greetings window.' 180 | ), 181 | 'TITLE_GREETINGS': ( 182 | 'Welcome to Free Our Knowledge!', 183 | 'Title for the pop-up greetings window.' 184 | ), 185 | 'TEXT_GREETINGS': ( 186 | 'This is Free Our Knowledge, give me your mail NOW:', 187 | 'Text for the pop-up greetings window.' 188 | ), 189 | 'SHOW_SIGNUP_MAILING_ON_GREETINGS': ( 190 | True, 191 | 'Check this if you want to show the sign up for the mailing list on the greetings pop-up.' 192 | ), 193 | 'COOKIES_TEXT': ( 194 | 'We use cookies to ensure that we give you the best experience on our website.', 195 | 'Add text for the cookies warning.' 196 | ), 197 | 'TWITTER_PROFILE': ( 198 | 'https://twitter.com/', '' 199 | ), 200 | 'FACEBOOK_PROFILE': ( 201 | 'https://www.facebook.com/', '' 202 | ), 203 | 'INSTAGRAM_PROFILE': ( 204 | 'https://www.instagram.com/', '' 205 | ), 206 | 'YOUTUBE_PROFILE': ( 207 | 'https://www.youtube.com/', '' 208 | ), 209 | 'GITHUB_PROFILE': ( 210 | 'https://www.github.com/', '' 211 | ) 212 | } 213 | 214 | SIGNUP_SEND_MAIL = False 215 | SIGNUP_ACTIVE_USER_BY_DEFAULT = True 216 | 217 | FIXTURES_PATH_TO_COVER_IMAGES = 'test-images/logos' 218 | 219 | # Storage Service 220 | 221 | AWS_ACCESS_KEY_ID = '' 222 | AWS_SECRET_ACCESS_KEY = '' 223 | AWS_STORAGE_BUCKET_NAME = 'codi.coop.test' 224 | AWS_S3_CUSTOM_DOMAIN = f's3.wasabisys.com/{AWS_STORAGE_BUCKET_NAME}' 225 | AWS_S3_ENDPOINT_URL = 'https://s3.wasabisys.com' 226 | AWS_DEFAULT_ACL = 'public-read' 227 | # DEFAULT_FILE_STORAGE = 'cc_lib.storages.MediaStorage' 228 | DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' 229 | EXTERNAL_MEDIA_PATH = 'fok/media' 230 | MEDIA_FILE_OVERWRITE = True 231 | -------------------------------------------------------------------------------- /apps/fok/static/styles/scss/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | // shared styles 2 | .btn, 3 | .btn-flat { 4 | border: $button-border; 5 | border-radius: $button-radius; 6 | display: inline-block; 7 | height: $button-height; 8 | line-height: $button-height; 9 | padding: $button-padding; 10 | text-transform: uppercase; 11 | vertical-align: middle; 12 | -webkit-tap-highlight-color: transparent; // Gets rid of tap active state 13 | } 14 | 15 | // Disabled shared style 16 | .btn.disabled, 17 | .btn-floating.disabled, 18 | .btn-large.disabled, 19 | .btn-small.disabled, 20 | .btn-flat.disabled, 21 | .btn:disabled, 22 | .btn-floating:disabled, 23 | .btn-large:disabled, 24 | .btn-small:disabled, 25 | .btn-flat:disabled, 26 | .btn[disabled], 27 | .btn-floating[disabled], 28 | .btn-large[disabled], 29 | .btn-small[disabled], 30 | .btn-flat[disabled] { 31 | pointer-events: none; 32 | background-color: $button-disabled-background !important; 33 | box-shadow: none; 34 | color: $button-disabled-color !important; 35 | cursor: default; 36 | &:hover { 37 | background-color: $button-disabled-background !important; 38 | color: $button-disabled-color !important; 39 | } 40 | } 41 | 42 | // Shared icon styles 43 | .btn, 44 | .btn-floating, 45 | .btn-large, 46 | .btn-small, 47 | .btn-flat { 48 | font-size: $button-font-size; 49 | outline: 0; 50 | i { 51 | font-size: $button-icon-font-size; 52 | line-height: inherit; 53 | } 54 | } 55 | 56 | // Shared focus button style 57 | .btn, 58 | .btn-floating { 59 | &:focus { 60 | background-color: darken($button-raised-background, 10%); 61 | } 62 | } 63 | 64 | // Raised Button 65 | .btn { 66 | text-decoration: none; 67 | color: $button-raised-color; 68 | background-color: $button-raised-background; 69 | text-align: center; 70 | letter-spacing: .5px; 71 | @extend .z-depth-1; 72 | transition: background-color .2s ease-out; 73 | cursor: pointer; 74 | &:hover { 75 | background-color: $button-raised-background-hover; 76 | @extend .z-depth-1-half; 77 | } 78 | } 79 | 80 | // Floating button 81 | .btn-floating { 82 | &:hover { 83 | background-color: $button-floating-background-hover; 84 | @extend .z-depth-1-half; 85 | } 86 | &:before { 87 | border-radius: 0; 88 | } 89 | &.btn-large { 90 | &.halfway-fab { 91 | bottom: -$button-floating-large-size / 2; 92 | } 93 | width: $button-floating-large-size; 94 | height: $button-floating-large-size; 95 | padding: 0; 96 | i { 97 | line-height: $button-floating-large-size; 98 | } 99 | } 100 | 101 | &.btn-small { 102 | &.halfway-fab { 103 | bottom: -$button-floating-small-size / 2; 104 | } 105 | width: $button-floating-small-size; 106 | height: $button-floating-small-size; 107 | i { 108 | line-height: $button-floating-small-size; 109 | } 110 | } 111 | 112 | &.halfway-fab { 113 | &.left { 114 | right: auto; 115 | left: 24px; 116 | } 117 | position: absolute; 118 | right: 24px; 119 | bottom: -$button-floating-size / 2; 120 | } 121 | display: inline-block; 122 | color: $button-floating-color; 123 | position: relative; 124 | overflow: hidden; 125 | z-index: 1; 126 | width: $button-floating-size; 127 | height: $button-floating-size; 128 | line-height: $button-floating-size; 129 | padding: 0; 130 | background-color: $button-floating-background; 131 | border-radius: $button-floating-radius; 132 | @extend .z-depth-1; 133 | transition: background-color .3s; 134 | cursor: pointer; 135 | vertical-align: middle; 136 | i { 137 | width: inherit; 138 | display: inline-block; 139 | text-align: center; 140 | color: $button-floating-color; 141 | font-size: $button-large-icon-font-size; 142 | line-height: $button-floating-size; 143 | } 144 | } 145 | 146 | // button fix 147 | button.btn-floating { 148 | border: $button-border; 149 | } 150 | 151 | // Fixed Action Button 152 | .fixed-action-btn { 153 | &.active { 154 | ul { 155 | visibility: visible; 156 | } 157 | } 158 | 159 | // Directions 160 | &.direction-left, 161 | &.direction-right { 162 | padding: 0 0 0 15px; 163 | ul { 164 | text-align: right; 165 | right: 64px; 166 | top: 50%; 167 | transform: translateY(-50%); 168 | height: 100%; 169 | left: auto; 170 | /*width 100% only goes to width of button container */ 171 | width: 500px; 172 | li { 173 | display: inline-block; 174 | margin: 7.5px 15px 0 0; 175 | } 176 | } 177 | } 178 | &.direction-right { 179 | padding: 0 15px 0 0; 180 | ul { 181 | text-align: left; 182 | direction: rtl; 183 | left: 64px; 184 | right: auto; 185 | li { 186 | margin: 7.5px 0 0 15px; 187 | } 188 | } 189 | } 190 | &.direction-bottom { 191 | padding: 0 0 15px 0; 192 | ul { 193 | top: 64px; 194 | bottom: auto; 195 | display: flex; 196 | flex-direction: column-reverse; 197 | li { 198 | margin: 15px 0 0 0; 199 | } 200 | } 201 | } 202 | &.toolbar { 203 | &.active { 204 | &>a i { 205 | opacity: 0; 206 | } 207 | } 208 | padding: 0; 209 | height: $button-floating-large-size; 210 | ul { 211 | display: flex; 212 | top: 0; 213 | bottom: 0; 214 | z-index: 1; 215 | li { 216 | flex: 1; 217 | display: inline-block; 218 | margin: 0; 219 | height: 100%; 220 | transition: none; 221 | a { 222 | display: block; 223 | overflow: hidden; 224 | position: relative; 225 | width: 100%; 226 | height: 100%; 227 | background-color: transparent; 228 | box-shadow: none; 229 | color: #fff; 230 | line-height: $button-floating-large-size; 231 | z-index: 1; 232 | i { 233 | line-height: inherit; 234 | } 235 | } 236 | } 237 | } 238 | } 239 | position: fixed; 240 | right: 23px; 241 | bottom: 23px; 242 | padding-top: 15px; 243 | margin-bottom: 0; 244 | z-index: 997; 245 | ul { 246 | left: 0; 247 | right: 0; 248 | text-align: center; 249 | position: absolute; 250 | bottom: 64px; 251 | margin: 0; 252 | visibility: hidden; 253 | li { 254 | margin-bottom: 15px; 255 | } 256 | a.btn-floating { 257 | opacity: 0; 258 | } 259 | } 260 | .fab-backdrop { 261 | position: absolute; 262 | top: 0; 263 | left: 0; 264 | z-index: -1; 265 | width: $button-floating-size; 266 | height: $button-floating-size; 267 | background-color: $button-floating-background; 268 | border-radius: $button-floating-radius; 269 | transform: scale(0); 270 | } 271 | } 272 | 273 | // Flat button 274 | .btn-flat { 275 | box-shadow: none; 276 | background-color: transparent; 277 | color: $button-flat-color; 278 | cursor: pointer; 279 | transition: background-color .2s; 280 | &:focus, 281 | &:hover { 282 | box-shadow: none; 283 | } 284 | &:focus { 285 | background-color: rgba(0, 0, 0, .1); 286 | } 287 | &.disabled, 288 | &.btn-flat[disabled] { 289 | background-color: transparent !important; 290 | color: $button-flat-disabled-color !important; 291 | cursor: default; 292 | } 293 | } 294 | 295 | // Large button 296 | .btn-large { 297 | @extend .btn; 298 | height: $button-large-height; 299 | line-height: $button-large-height; 300 | font-size: $button-large-font-size; 301 | padding: 0 28px; 302 | 303 | i { 304 | font-size: $button-large-icon-font-size; 305 | } 306 | } 307 | 308 | // Small button 309 | .btn-small { 310 | @extend .btn; 311 | height: $button-small-height; 312 | line-height: $button-small-height; 313 | font-size: $button-small-font-size; 314 | i { 315 | font-size: $button-small-icon-font-size; 316 | } 317 | } 318 | 319 | // Block button 320 | .btn-block { 321 | display: block; 322 | } 323 | --------------------------------------------------------------------------------