diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97298ffca..28bfcc5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,18 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v2 + - name: Install platform dependencies + run: | + sudo apt -y update + sudo apt -y install --no-install-recommends \ + texlive-latex-base \ + texlive-latex-recommended \ + texlive-plain-generic \ + lmodern + - name: Install pandoc + run: | + wget https://github.com/jgm/pandoc/releases/download/2.17.1.1/pandoc-2.17.1.1-1-amd64.deb + sudo dpkg -i pandoc-2.17.1.1-1-amd64.deb - uses: actions/setup-python@v2 with: python-version: 3.9.16 diff --git a/Aptfile b/Aptfile new file mode 100644 index 000000000..e69de29bb diff --git a/Dockerfile b/Dockerfile index 4d1046a98..a3c351f5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,48 @@ -FROM python:3.9-bullseye +FROM python:3.9-bookworm ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 + +# By default, Docker has special steps to avoid keeping APT caches in the layers, which +# is good, but in our case, we're going to mount a special cache volume (kept between +# builds), so we WANT the cache to persist. +RUN set -eux; \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; + +# Install System level build requirements, this is done before +# everything else because these are rarely ever going to change. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + set -x \ + && apt-get update \ + && apt-get install --no-install-recommends -y \ + texlive-latex-base \ + texlive-latex-recommended \ + texlive-fonts-recommended \ + texlive-plain-generic \ + lmodern + +RUN case $(uname -m) in \ + "x86_64") ARCH=amd64 ;; \ + "aarch64") ARCH=arm64 ;; \ + esac \ + && wget --quiet https://github.com/jgm/pandoc/releases/download/2.17.1.1/pandoc-2.17.1.1-1-${ARCH}.deb \ + && dpkg -i pandoc-2.17.1.1-1-${ARCH}.deb + RUN mkdir /code WORKDIR /code + COPY dev-requirements.txt /code/ COPY base-requirements.txt /code/ -RUN pip install -r dev-requirements.txt +COPY prod-requirements.txt /code/ +COPY requirements.txt /code/ + +RUN pip --no-cache-dir --disable-pip-version-check install --upgrade pip setuptools wheel + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -x \ + && pip --disable-pip-version-check \ + install \ + -r dev-requirements.txt + COPY . /code/ diff --git a/Dockerfile.cabotage b/Dockerfile.cabotage new file mode 100644 index 000000000..d96e002a7 --- /dev/null +++ b/Dockerfile.cabotage @@ -0,0 +1,49 @@ +FROM python:3.9-bullseye +COPY --from=ewdurbin/nginx-static:1.25.x /usr/bin/nginx /usr/bin/nginx +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# By default, Docker has special steps to avoid keeping APT caches in the layers, which +# is good, but in our case, we're going to mount a special cache volume (kept between +# builds), so we WANT the cache to persist. +RUN set -eux; \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; + +# Install System level build requirements, this is done before +# everything else because these are rarely ever going to change. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + set -x \ + && apt-get update \ + && apt-get install --no-install-recommends -y \ + texlive-latex-base \ + texlive-latex-recommended \ + texlive-fonts-recommended \ + texlive-plain-generic \ + lmodern + +RUN case $(uname -m) in \ + "x86_64") ARCH=amd64 ;; \ + "aarch64") ARCH=arm64 ;; \ + esac \ + && wget --quiet https://github.com/jgm/pandoc/releases/download/2.17.1.1/pandoc-2.17.1.1-1-${ARCH}.deb \ + && dpkg -i pandoc-2.17.1.1-1-${ARCH}.deb + +RUN mkdir /code +WORKDIR /code + +COPY dev-requirements.txt /code/ +COPY base-requirements.txt /code/ +COPY prod-requirements.txt /code/ +COPY requirements.txt /code/ + +RUN pip --no-cache-dir --disable-pip-version-check install --upgrade pip setuptools wheel + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -x \ + && pip --disable-pip-version-check \ + install \ + -r requirements.txt -r prod-requirements.txt +COPY . /code/ +RUN DJANGO_SETTINGS_MODULE=pydotorg.settings.static python manage.py collectstatic --noinput diff --git a/Procfile b/Procfile index 651bc19b8..16deb5f5b 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,4 @@ release: python manage.py migrate --noinput web: bin/start-nginx gunicorn -c gunicorn.conf pydotorg.wsgi +worker: celery -A pydotorg worker -l INFO +worker-beat: celery -A pydotorg beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler diff --git a/base-requirements.txt b/base-requirements.txt index 9b21bccd6..7a7823864 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -19,6 +19,8 @@ feedparser==6.0.8 beautifulsoup4==4.11.2 icalendar==4.0.7 chardet==4.0.0 +celery[redis]==5.3.6 +django-celery-beat==2.5.0 # TODO: We may drop 'django-imagekit' completely. django-imagekit==4.0.2 django-haystack==3.2.1 @@ -44,12 +46,11 @@ django-filter==2.4.0 django-ordered-model==3.4.3 django-widget-tweaks==1.4.8 django-countries==7.2.1 -xhtml2pdf==0.2.5 -django-easy-pdf3==0.1.2 num2words==0.5.10 django-polymorphic==3.0.0 sorl-thumbnail==12.7.0 -docxtpl==0.12.0 -reportlab==3.6.6 django-extensions==3.1.4 django-import-export==2.7.1 + +pypandoc==1.12 +panflute==2.3.0 diff --git a/bin/start-nginx b/bin/start-nginx new file mode 100755 index 000000000..6ffacb572 --- /dev/null +++ b/bin/start-nginx @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +psmgr=/tmp/nginx-buildpack-wait +rm -f $psmgr +mkfifo $psmgr + +n=1 +while getopts :f option ${@:1:2} +do + case "${option}" + in + f) FORCE=$OPTIND; n=$((n+1));; + esac +done + +# Initialize log directory. +mkdir -p /tmp/logs/nginx +touch /tmp/logs/nginx/access.log /tmp/logs/nginx/error.log +echo 'buildpack=nginx at=logs-initialized' + +# Start log redirection. +( + # Redirect nginx logs to stdout. + tail -qF -n 0 /tmp/logs/nginx/*.log + echo 'logs' >$psmgr +) & + +# Start App Server +( + # Take the command passed to this bin and start it. + # E.g. bin/start-nginx bundle exec unicorn -c config/unicorn.rb + COMMAND=${@:$n} + echo "buildpack=nginx at=start-app cmd=$COMMAND" + $COMMAND + echo 'app' >$psmgr +) & + +if [[ -z "$FORCE" ]] +then + FILE="/tmp/app-initialized" + + # We block on app-initialized so that when nginx binds to $PORT + # are app is ready for traffic. + while [[ ! -f "$FILE" ]] + do + echo 'buildpack=nginx at=app-initialization' + sleep 1 + done + echo 'buildpack=nginx at=app-initialized' +fi + +# Start nginx +( + # We expect nginx to run in foreground. + # We also expect a socket to be at /tmp/nginx.socket. + echo 'buildpack=nginx at=nginx-start' + cd /tmp + /usr/bin/nginx -p . -c /code/config/nginx.conf + echo 'nginx' >$psmgr +) & + +# This read will block the process waiting on a msg to be put into the fifo. +# If any of the processes defined above should exit, +# a msg will be put into the fifo causing the read operation +# to un-block. The process putting the msg into the fifo +# will use it's process name as a msg so that we can print the offending +# process to stdout. +read exit_process <$psmgr +echo "buildpack=nginx at=exit process=$exit_process" +exit 1 diff --git a/config/mime.types b/config/mime.types new file mode 100644 index 000000000..8d37c8636 --- /dev/null +++ b/config/mime.types @@ -0,0 +1,98 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/avif avif; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/config/nginx.conf.erb b/config/nginx.conf similarity index 94% rename from config/nginx.conf.erb rename to config/nginx.conf index 42bbf94cc..5ee6aaf6b 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf @@ -1,6 +1,5 @@ daemon off; -#Heroku dynos have at least 4 cores. -worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>; +worker_processes 2; events { use epoll; @@ -15,9 +14,8 @@ http { server_tokens off; - log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; - access_log logs/nginx/access.log l2met; - error_log logs/nginx/error.log; + access_log /tmp/logs/nginx/access.log; + error_log /tmp/logs/nginx/error.log; include mime.types; default_type application/octet-stream; @@ -29,11 +27,11 @@ http { client_max_body_size 32m; upstream app_server { - server unix:/tmp/nginx.socket fail_timeout=0; + server unix:/var/run/cabotage/nginx.sock fail_timeout=0; } server { - listen <%= ENV["PORT"] %>; + listen unix:/var/run/cabotage/cabotage.sock; server_name _; keepalive_timeout 5; @@ -208,19 +206,19 @@ http { return 301 https://www.python.org/download/windows/; } - location /download/ { + location ~ ^/download/$ { return 301 https://www.python.org/downloads/; } - location /download/source/ { + location ~ ^/download/source/$ { return 301 https://www.python.org/downloads/source/; } - location /download/mac/ { + location ~ ^/download/mac/$ { return 301 https://www.python.org/downloads/macos/; } - location /download/windows/ { + location ~ ^/download/windows/$ { return 301 https://www.python.org/downloads/windows/; } @@ -317,17 +315,17 @@ http { } location /static/ { - alias /app/static-root/; + alias /code/static-root/; add_header Cache-Control "max-age=604800, public"; # 604800 is 7 days } location /images/ { - alias /app/static-root/images/; + alias /code/static-root/images/; add_header Cache-Control "max-age=604800, public"; # 604800 is 7 days } location /favicon.ico { - alias /app/static-root/favicon.ico; + alias /code/static-root/favicon.ico; add_header Cache-Control "max-age=604800, public"; # 604800 is 7 days } diff --git a/docker-compose.yml b/docker-compose.yml index 22221629c..2e5f8bf16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,17 @@ services: test: ["CMD", "pg_isready", "-U", "pythondotorg", "-d", "pythondotorg"] interval: 1s + redis: + image: redis:7-bullseye + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli","ping"] + interval: 1s + web: build: . + image: pythondotorg:docker-compose command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code @@ -27,3 +36,19 @@ services: depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy + + worker: + image: pythondotorg:docker-compose + command: celery -A pydotorg worker -B -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler + volumes: + - .:/code + environment: + DATABASE_URL: postgresql://pythondotorg:pythondotorg@postgres:5432/pythondotorg + DJANGO_SETTINGS_MODULE: pydotorg.settings.local + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy diff --git a/gunicorn.conf b/gunicorn.conf index a68960607..74207d515 100644 --- a/gunicorn.conf +++ b/gunicorn.conf @@ -1,4 +1,4 @@ -bind = 'unix:/tmp/nginx.socket' +bind = 'unix:/var/run/cabotage/nginx.sock' backlog = 1024 preload_app = True max_requests = 2048 diff --git a/pydotorg/__init__.py b/pydotorg/__init__.py index e69de29bb..3307b5134 100644 --- a/pydotorg/__init__.py +++ b/pydotorg/__init__.py @@ -0,0 +1,3 @@ +from pydotorg.celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/pydotorg/celery.py b/pydotorg/celery.py new file mode 100644 index 000000000..51062cf9b --- /dev/null +++ b/pydotorg/celery.py @@ -0,0 +1,15 @@ +import os + +from celery import Celery +from django.core import management + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pydotorg.settings.local") + +app = Celery("pydotorg") +app.config_from_object("django.conf:settings", namespace="CELERY") + +@app.task(bind=True) +def run_management_command(self, command_name, args, kwargs): + management.call_command(command_name, *args, **kwargs) + +app.autodiscover_tasks() diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index 25874dd5d..a602dff7e 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -31,6 +31,23 @@ ) } +# celery settings +_REDIS_URL = config("REDIS_URL", default="redis://redis:6379/0") + +CELERY_BROKER_URL = _REDIS_URL +CELERY_RESULT_BACKEND = _REDIS_URL + +CELERY_BEAT_SCHEDULE = { + # "example-management-command": { + # "task": "pydotorg.celery.run_management_command", + # "schedule": crontab(hour=12, minute=0), + # "args": ("daily_volunteer_reminder", [], {}), + # }, + # 'example-task': { + # 'task': 'users.tasks.example_task', + # }, +} + ### Locale settings TIME_ZONE = 'UTC' @@ -163,6 +180,7 @@ 'django.contrib.admin', 'django.contrib.admindocs', + 'django_celery_beat', 'django_translation_aliases', 'pipeline', 'sitetree', @@ -173,7 +191,6 @@ 'ordered_model', 'widget_tweaks', 'django_countries', - 'easy_pdf', 'sorl.thumbnail', 'banners', diff --git a/pydotorg/settings/heroku.py b/pydotorg/settings/cabotage.py similarity index 94% rename from pydotorg/settings/heroku.py rename to pydotorg/settings/cabotage.py index 83e975cb1..d73beb83c 100644 --- a/pydotorg/settings/heroku.py +++ b/pydotorg/settings/cabotage.py @@ -29,6 +29,9 @@ 'ENGINE': 'haystack.backends.elasticsearch7_backend.Elasticsearch7SearchEngine', 'URL': HAYSTACK_SEARCHBOX_SSL_URL, 'INDEX_NAME': config('HAYSTACK_INDEX', default='haystack-prod'), + 'KWARGS': { + 'ca_certs': '/var/run/secrets/cabotage.io/ca.crt', + } }, } @@ -68,7 +71,7 @@ RAVEN_CONFIG = { "dsn": config('SENTRY_DSN'), - "release": config('SOURCE_VERSION'), + "release": config('SOURCE_COMMIT'), } AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') diff --git a/pydotorg/settings/static.py b/pydotorg/settings/static.py new file mode 100644 index 000000000..3d93e113e --- /dev/null +++ b/pydotorg/settings/static.py @@ -0,0 +1,25 @@ +import os + +import dj_database_url +import raven +from decouple import Csv + +from .base import * + +DEBUG = TEMPLATE_DEBUG = False + +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'haystack.backends.elasticsearch5_backend.Elasticsearch5SearchEngine', + 'URL': 'http://127.0.0.1:9200', + 'INDEX_NAME': 'haystack-null', + }, +} + +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', +] + MIDDLEWARE + +MEDIAFILES_LOCATION = 'media' +DEFAULT_FILE_STORAGE = 'custom_storages.storages.MediaStorage' +STATICFILES_STORAGE = 'custom_storages.storages.PipelineManifestStorage' diff --git a/pydotorg/urls.py b/pydotorg/urls.py index c112b2e19..f6ee8001d 100644 --- a/pydotorg/urls.py +++ b/pydotorg/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ # homepage path('', views.IndexView.as_view(), name='home'), + re_path(r'^_health/?', views.health, name='health'), path('authenticated', views.AuthenticatedView.as_view(), name='authenticated'), re_path(r'^humans.txt$', TemplateView.as_view(template_name='humans.txt', content_type='text/plain')), re_path(r'^robots.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')), diff --git a/pydotorg/views.py b/pydotorg/views.py index 476e62fd9..9777cf1aa 100644 --- a/pydotorg/views.py +++ b/pydotorg/views.py @@ -1,10 +1,15 @@ from django.conf import settings +from django.http import HttpResponse from django.views.generic.base import RedirectView, TemplateView from codesamples.models import CodeSample from downloads.models import Release +def health(request): + return HttpResponse('OK') + + class IndexView(TemplateView): template_name = "python/index.html" diff --git a/sponsors/contracts.py b/sponsors/contracts.py new file mode 100644 index 000000000..2720ec1c8 --- /dev/null +++ b/sponsors/contracts.py @@ -0,0 +1,88 @@ +import os +import tempfile + +from django.http import HttpResponse +from django.template.loader import render_to_string +from django.utils.dateformat import format + +import pypandoc + +dirname = os.path.dirname(__file__) +DOCXPAGEBREAK_FILTER = os.path.join(dirname, "pandoc_filters/pagebreak.py") +REFERENCE_DOCX = os.path.join(dirname, "reference.docx") + + +def _clean_split(text, separator="\n"): + return [ + t.replace("-", "").strip() + for t in text.split("\n") + if t.replace("-", "").strip() + ] + + +def _contract_context(contract, **context): + start_date = contract.sponsorship.start_date + context.update( + { + "contract": contract, + "start_date": start_date, + "start_day_english_suffix": format(start_date, "S"), + "sponsor": contract.sponsorship.sponsor, + "sponsorship": contract.sponsorship, + "benefits": _clean_split(contract.benefits_list.raw), + "legal_clauses": _clean_split(contract.legal_clauses.raw), + "renewal": True if contract.sponsorship.renewal else False, + } + ) + previous_effective = contract.sponsorship.previous_effective_date + context["previous_effective"] = previous_effective if previous_effective else "UNKNOWN" + context["previous_effective_english_suffix"] = format(previous_effective, "S") if previous_effective else "UNKNOWN" + return context + + +def render_markdown_from_template(contract, **context): + template = "sponsors/admin/contracts/sponsorship-agreement.md" + context = _contract_context(contract, **context) + return render_to_string(template, context) + + +def render_contract_to_pdf_response(request, contract, **context): + response = HttpResponse( + render_contract_to_pdf_file(contract, **context), content_type="application/pdf" + ) + return response + + +def render_contract_to_pdf_file(contract, **context): + with tempfile.NamedTemporaryFile() as docx_file: + with tempfile.NamedTemporaryFile(suffix=".pdf") as pdf_file: + markdown = render_markdown_from_template(contract, **context) + pdf = pypandoc.convert_text( + markdown, "pdf", outputfile=pdf_file.name, format="md" + ) + return pdf_file.read() + + +def render_contract_to_docx_response(request, contract, **context): + response = HttpResponse( + render_contract_to_docx_file(contract, **context), + content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + response[ + "Content-Disposition" + ] = f"attachment; filename={'sponsorship-renewal' if contract.sponsorship.renewal else 'sponsorship-contract'}-{contract.sponsorship.sponsor.name.replace(' ', '-').replace('.', '')}.docx" + return response + + +def render_contract_to_docx_file(contract, **context): + markdown = render_markdown_from_template(contract, **context) + with tempfile.NamedTemporaryFile() as docx_file: + docx = pypandoc.convert_text( + markdown, + "docx", + outputfile=docx_file.name, + format="md", + filters=[DOCXPAGEBREAK_FILTER], + extra_args=[f"--reference-doc", REFERENCE_DOCX], + ) + return docx_file.read() diff --git a/sponsors/pandoc_filters/__init__.py b/sponsors/pandoc_filters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sponsors/pandoc_filters/pagebreak.py b/sponsors/pandoc_filters/pagebreak.py new file mode 100644 index 000000000..22a786a2b --- /dev/null +++ b/sponsors/pandoc_filters/pagebreak.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# Source: https://github.com/pandocker/pandoc-docx-pagebreak-py/ +# Revision: c8cddccebb78af75168da000a3d6ac09349bef73 +# ------------------------------------------------------------------------------ +# MIT License +# +# Copyright (c) 2018 pandocker +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ------------------------------------------------------------------------------ + +""" pandoc-docx-pagebreakpy +Pandoc filter to insert pagebreak as openxml RawBlock +Only for docx output + +Trying to port pandoc-doc-pagebreak +- https://github.com/alexstoick/pandoc-docx-pagebreak +""" + +import panflute as pf + + +class DocxPagebreak(object): + pagebreak = pf.RawBlock("", format="openxml") + sectionbreak = pf.RawBlock("", + format="openxml") + toc = pf.RawBlock(r""" + + + + + + TOC \o "1-3" \h \z \u + + + + + + +""", format="openxml") + + def action(self, elem, doc): + if isinstance(elem, pf.RawBlock): + if elem.text == r"\newpage": + if (doc.format == "docx"): + elem = self.pagebreak + # elif elem.text == r"\newsection": + # if (doc.format == "docx"): + # pf.debug("Section Break") + # elem = self.sectionbreak + # else: + # elem = [] + elif elem.text == r"\toc": + if (doc.format == "docx"): + pf.debug("Table of Contents") + para = [pf.Para(pf.Str("Table"), pf.Space(), pf.Str("of"), pf.Space(), pf.Str("Contents"))] + div = pf.Div(*para, attributes={"custom-style": "TOC Heading"}) + elem = [div, self.toc] + else: + elem = [] + return elem + + +def main(doc=None): + dp = DocxPagebreak() + return pf.run_filter(dp.action, doc=doc) + + +if __name__ == "__main__": + main() diff --git a/sponsors/pdf.py b/sponsors/pdf.py deleted file mode 100644 index 9855beee3..000000000 --- a/sponsors/pdf.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -This module is a wrapper around django-easy-pdf so we can reuse code -""" -import io -import os -from django.conf import settings -from django.http import HttpResponse -from django.utils.dateformat import format - -from docxtpl import DocxTemplate -from easy_pdf.rendering import render_to_pdf_response, render_to_pdf - -from markupfield_helpers.helpers import render_md -from django.utils.html import mark_safe - - -def _clean_split(text, separator='\n'): - return [ - t.replace('-', '').strip() - for t in text.split('\n') - if t.replace('-', '').strip() - ] - - -def _contract_context(contract, **context): - start_date = contract.sponsorship.start_date - context.update({ - "contract": contract, - "start_date": start_date, - "start_day_english_suffix": format(start_date, "S"), - "sponsor": contract.sponsorship.sponsor, - "sponsorship": contract.sponsorship, - "benefits": _clean_split(contract.benefits_list.raw), - "legal_clauses": _clean_split(contract.legal_clauses.raw), - "renewal": contract.sponsorship.renewal, - }) - previous_effective = contract.sponsorship.previous_effective_date - context["previous_effective"] = previous_effective if previous_effective else "UNKNOWN" - context["previous_effective_english_suffix"] = format(previous_effective, "S") if previous_effective else None - return context - - -def render_contract_to_pdf_response(request, contract, **context): - template = "sponsors/admin/preview-contract.html" - context = _contract_context(contract, **context) - return render_to_pdf_response(request, template, context) - - -def render_contract_to_pdf_file(contract, **context): - template = "sponsors/admin/preview-contract.html" - context = _contract_context(contract, **context) - return render_to_pdf(template, context) - - -def _gen_docx_contract(output, contract, **context): - context = _contract_context(contract, **context) - renewal = context["renewal"] - if renewal: - template = os.path.join(settings.TEMPLATES_DIR, "sponsors", "admin", "renewal-contract-template.docx") - else: - template = os.path.join(settings.TEMPLATES_DIR, "sponsors", "admin", "contract-template.docx") - doc = DocxTemplate(template) - doc.render(context) - doc.save(output) - return output - - -def render_contract_to_docx_response(request, contract, **context): - response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document') - response['Content-Disposition'] = 'attachment; filename=contract.docx' - return _gen_docx_contract(output=response, contract=contract, **context) - - -def render_contract_to_docx_file(contract, **context): - fp = io.BytesIO() - fp = _gen_docx_contract(output=fp, contract=contract, **context) - fp.seek(0) - return fp.read() diff --git a/sponsors/reference.docx b/sponsors/reference.docx new file mode 100644 index 000000000..9e2df1a41 Binary files /dev/null and b/sponsors/reference.docx differ diff --git a/sponsors/tests/test_contracts.py b/sponsors/tests/test_contracts.py new file mode 100644 index 000000000..c330c13a8 --- /dev/null +++ b/sponsors/tests/test_contracts.py @@ -0,0 +1,39 @@ +from datetime import date +from model_bakery import baker +from unittest.mock import patch, Mock + +from django.http import HttpRequest +from django.test import TestCase +from django.utils.dateformat import format + +from sponsors.contracts import render_contract_to_docx_response + + +class TestRenderContract(TestCase): + def setUp(self): + self.contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today()) + + # DOCX unit test + def test_render_response_with_docx_attachment(self): + request = Mock(HttpRequest) + self.contract.sponsorship.renewal = False + response = render_contract_to_docx_response(request, self.contract) + + self.assertEqual(response.get("Content-Disposition"), "attachment; filename=sponsorship-contract-Sponsor.docx") + self.assertEqual( + response.get("Content-Type"), + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + + + # DOCX unit test + def test_render_renewal_response_with_docx_attachment(self): + request = Mock(HttpRequest) + self.contract.sponsorship.renewal = True + response = render_contract_to_docx_response(request, self.contract) + + self.assertEqual(response.get("Content-Disposition"), "attachment; filename=sponsorship-renewal-Sponsor.docx") + self.assertEqual( + response.get("Content-Type"), + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) diff --git a/sponsors/tests/test_pdf.py b/sponsors/tests/test_pdf.py deleted file mode 100644 index e4d140cf2..000000000 --- a/sponsors/tests/test_pdf.py +++ /dev/null @@ -1,113 +0,0 @@ -from datetime import date -from docxtpl import DocxTemplate -from markupfield_helpers.helpers import render_md -from model_bakery import baker -from pathlib import Path -from unittest.mock import patch, Mock - -from django.conf import settings -from django.http import HttpResponse, HttpRequest -from django.template.loader import render_to_string -from django.test import TestCase -from django.utils.html import mark_safe -from django.utils.dateformat import format - -from sponsors.pdf import render_contract_to_pdf_file, render_contract_to_pdf_response, render_contract_to_docx_response - - -class TestRenderContract(TestCase): - def setUp(self): - self.contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today()) - text = f"{self.contract.benefits_list.raw}\n\n**Legal Clauses**\n{self.contract.legal_clauses.raw}" - html = render_md(text) - self.context = { - "contract": self.contract, - "start_date": self.contract.sponsorship.start_date, - "start_day_english_suffix": format(self.contract.sponsorship.start_date, "S"), - "sponsor": self.contract.sponsorship.sponsor, - "sponsorship": self.contract.sponsorship, - "benefits": [], - "legal_clauses": [], - "renewal": None, - "previous_effective": "UNKNOWN", - "previous_effective_english_suffix": None, - } - self.template = "sponsors/admin/preview-contract.html" - - # PDF unit tests - @patch("sponsors.pdf.render_to_pdf") - def test_render_pdf_using_django_easy_pdf(self, mock_render): - mock_render.return_value = "pdf content" - - content = render_contract_to_pdf_file(self.contract) - - self.assertEqual(content, "pdf content") - mock_render.assert_called_once_with(self.template, self.context) - - @patch("sponsors.pdf.render_to_pdf_response") - def test_render_response_using_django_easy_pdf(self, mock_render): - response = Mock(HttpResponse) - mock_render.return_value = response - - request = Mock(HttpRequest) - content = render_contract_to_pdf_response(request, self.contract) - - self.assertEqual(content, response) - mock_render.assert_called_once_with(request, self.template, self.context) - - # DOCX unit test - @patch("sponsors.pdf.DocxTemplate") - def test_render_response_with_docx_attachment(self, MockDocxTemplate): - template = Path(settings.TEMPLATES_DIR) / "sponsors" / "admin" / "contract-template.docx" - self.assertTrue(template.exists()) - mocked_doc = Mock(DocxTemplate) - MockDocxTemplate.return_value = mocked_doc - - request = Mock(HttpRequest) - response = render_contract_to_docx_response(request, self.contract) - - MockDocxTemplate.assert_called_once_with(str(template.resolve())) - mocked_doc.render.assert_called_once_with(self.context) - mocked_doc.save.assert_called_once_with(response) - self.assertEqual(response.get("Content-Disposition"), "attachment; filename=contract.docx") - self.assertEqual( - response.get("Content-Type"), - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ) - - @patch("sponsors.pdf.DocxTemplate") - def test_render_response_with_docx_attachment__renewal(self, MockDocxTemplate): - renewal_contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today(), - sponsorship__renewal=True) - text = f"{renewal_contract.benefits_list.raw}\n\n**Legal Clauses**\n{renewal_contract.legal_clauses.raw}" - html = render_md(text) - renewal_context = { - "contract": renewal_contract, - "start_date": renewal_contract.sponsorship.start_date, - "start_day_english_suffix": format(self.contract.sponsorship.start_date, "S"), - "sponsor": renewal_contract.sponsorship.sponsor, - "sponsorship": renewal_contract.sponsorship, - "benefits": [], - "legal_clauses": [], - "renewal": True, - "previous_effective": "UNKNOWN", - "previous_effective_english_suffix": None, - } - renewal_template = "sponsors/admin/preview-contract.html" - - template = Path(settings.TEMPLATES_DIR) / "sponsors" / "admin" / "renewal-contract-template.docx" - self.assertTrue(template.exists()) - mocked_doc = Mock(DocxTemplate) - MockDocxTemplate.return_value = mocked_doc - - request = Mock(HttpRequest) - response = render_contract_to_docx_response(request, renewal_contract) - - MockDocxTemplate.assert_called_once_with(str(template.resolve())) - mocked_doc.render.assert_called_once_with(renewal_context) - mocked_doc.save.assert_called_once_with(response) - self.assertEqual(response.get("Content-Disposition"), "attachment; filename=contract.docx") - self.assertEqual( - response.get("Content-Type"), - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ) diff --git a/sponsors/use_cases.py b/sponsors/use_cases.py index bbb6f2483..91271ff64 100644 --- a/sponsors/use_cases.py +++ b/sponsors/use_cases.py @@ -3,7 +3,7 @@ from sponsors import notifications from sponsors.models import Sponsorship, Contract, SponsorContact, SponsorEmailNotificationTemplate, SponsorshipBenefit, \ SponsorshipPackage -from sponsors.pdf import render_contract_to_pdf_file, render_contract_to_docx_file +from sponsors.contracts import render_contract_to_pdf_file, render_contract_to_docx_file class BaseUseCaseWithNotifications: diff --git a/sponsors/views_admin.py b/sponsors/views_admin.py index e9a808ccc..fd8631d3f 100644 --- a/sponsors/views_admin.py +++ b/sponsors/views_admin.py @@ -14,7 +14,7 @@ from sponsors.forms import SponsorshipReviewAdminForm, SponsorshipsListForm, SignedSponsorshipReviewAdminForm, \ SendSponsorshipNotificationForm, CloneApplicationConfigForm from sponsors.exceptions import InvalidStatusException -from sponsors.pdf import render_contract_to_pdf_response, render_contract_to_docx_response +from sponsors.contracts import render_contract_to_pdf_response, render_contract_to_docx_response from sponsors.models import Sponsorship, SponsorBenefit, EmailTargetable, SponsorContact, BenefitFeature, \ SponsorshipCurrentYear, SponsorshipBenefit, SponsorshipPackage diff --git a/templates/sponsors/admin/contract-template.docx b/templates/sponsors/admin/contract-template.docx deleted file mode 100644 index 5bdc44525..000000000 Binary files a/templates/sponsors/admin/contract-template.docx and /dev/null differ diff --git a/templates/sponsors/admin/contracts/sponsorship-agreement.md b/templates/sponsors/admin/contracts/sponsorship-agreement.md new file mode 100644 index 000000000..f1e2f2e9f --- /dev/null +++ b/templates/sponsors/admin/contracts/sponsorship-agreement.md @@ -0,0 +1,225 @@ +{% load humanize %} +--- +title: SPONSORSHIP AGREEMENT{% if renewal %} RENEWAL{% endif %} +geometry: +- margin=1.25in +font-size: 12pt +pagestyle: empty +header-includes: +- \pagenumbering{gobble} +--- + +**THIS SPONSORSHIP AGREEMENT{% if renewal %} RENEWAL{% endif %}** (the **"Agreement"**) +is entered into and made effective as of the +{{start_date|date:"j"}}{{start_day_english_suffix}} of {{start_date|date:"F Y"}} +(the **"Effective Date"**), +by and between the **Python Software Foundation** (the **"PSF"**), +a Delaware nonprofit corporation, +and **{{sponsor.name|upper}}** (**"Sponsor"**), +a {{sponsor.state}} corporation. +Each of the PSF and Sponsor are hereinafter sometimes individually +referred to as a **"Party"** and collectively as the **"Parties"**. + +## RECITALS + +**WHEREAS**, the PSF is a tax-exempt charitable organization (EIN 04-3594598) +whose mission is to promote, protect, and advance the Python programming language, +and to support and facilitate the growth of a diverse +and international community of Python programmers (the **"Programs"**); + +**WHEREAS**, Sponsor is {{contract.sponsor_info}}; and + +**WHEREAS**, Sponsor {% if renewal %} +and the PSF previously entered into a Sponsorship Agreement +with the effective date of the +{{ previous_effective|date:"j" }}{{ previous_effective_english_suffix }} of {{ previous_effective|date:"F Y" }} + +**WHEREAS**, Sponsor wishes to renew its support the Programs by making a contribution to the PSF. +and a term of one year (the “Sponsorship Agreement”). +{% else %} +wishes to support the Programs by making a contribution to the PSF. +{% endif %} + +## AGREEMENT + +**NOW, THEREFORE**, in consideration of the foregoing and the mutual covenants contained herein, and for other good and valuable consideration, the receipt and sufficiency of which are hereby acknowledged, the Parties hereto agree as follows: + +{% if renewal %} +1. [**Replacement of the Exhibit**]{.underline} Exhibit A to the Sponsorship Agreement is replaced with Exhibit A below. + +1. [**Renewal**]{.underline} Approval and incorporation of this new exhibit with the previous Sponsor Benefits shall be considered written notice by Sponsor to the PSF that you wish to continue the terms of the Sponsorship Agreement for an additional year and to contribute the new Sponsorship Payment specified in Exhibit A, beginning on the Effective Date, as contemplated by Section 6 of the Sponsorship Agreement. +{% else %} +1. [**Recitals Incorporated**]{.underline}. Each of the above Recitals is incorporated into and is made a part of this Agreement. + +1. [**Exhibits Incorporated by Reference**]{.underline}. All exhibits referenced in this Agreement are incorporated herein as integral parts of this Agreement and shall be considered reiterated herein as fully as if such provisions had been set forth verbatim in this Agreement. + +1. [**Sponsorship Payment**]{.underline}. In consideration for the right to sponsor the PSF and its Programs, and to be acknowledged by the PSF as a sponsor in the manner described herein, Sponsor shall make a contribution to the PSF (the "Sponsorship Payment") in the amount shown in Exhibit A. + +1. [**Acknowledgement of Sponsor**]{.underline}. In return for the Sponsorship Payment, Sponsor will be entitled to receive the sponsorship package described in Exhibit A attached hereto (the "Sponsor Benefits"). + +1. [**Intellectual Property**]{.underline}. The PSF is the sole owner of all right, title, and interest to all the PSF information, including the PSF’s logo, trademarks, trade names, and copyrighted information, unless otherwise provided. + + (a) [Grant of License by the PSF]{.underline}. The PSF hereby grants to Sponsor a limited, non- exclusive license to use certain of the PSF’s intellectual property, including the PSF’s name, acronym, and logo (collectively, the "PSF Intellectual Property"), solely in connection with promotion of Sponsor’s sponsorship of the Programs. Sponsor agrees that it shall not use the PSF’s Property in a manner that states or implies that the PSF endorses Sponsor (or Sponsor’s products or services). The PSF retains the right, in its sole and absolute discretion, to review and approve in advance all uses of the PSF Intellectual Property, which approval shall not be unreasonably withheld. + + (a) [Grant of License by Sponsor]{.underline}. Sponsor hereby grants to the PSF a limited, non-exclusive license to use certain of Sponsor’s intellectual property, including names, trademarks, and copyrights (collectively, "Sponsor Intellectual Property"), solely to identify Sponsor as a sponsor of the Programs and the PSF. Sponsor retains the right to review and approve in advance all uses of the Sponsor Intellectual Property, which approval shall not be unreasonably withheld. + +1. [**Term**]{.underline}. The Term of this Agreement will begin on the Effective Date and continue for a period of one (1) year. The Agreement may be renewed for one (1) year by written notice from Sponsor to the PSF. + +1. [**Termination**]{.underline}. The Agreement may be terminated (i) by either Party for any reason upon sixty (60) days prior written notice to the other Party; (ii) if one Party notifies the other Party that the other Party is in material breach of its obligations under this Agreement and such breach (if curable) is not cured with fifteen (15) days of such notice; (iii) if both Parties agree to terminate by mutual written consent; or (iv) if any of Sponsor information is found or is reasonably alleged to violate the rights of a third party. The PSF shall also have the unilateral right to terminate this Agreement at any time if it reasonably determines that it would be detrimental to the reputation and goodwill of the PSF or the Programs to continue to accept or use funds from Sponsor. Upon expiration or termination, no further use may be made by either Party of the other’s name, marks, logo or other intellectual property without the express prior written authorization of the other Party. + +1. [**Code of Conduct**]{.underline}. Sponsor and all of its representatives shall conduct themselves at all times in accordance with the Python Software Foundation Code of Conduct (https://www.python.org/psf/conduct) and/or the PyCon Code of Conduct (https://pycon.us/code-of-conduct), as applicable. The PSF reserves the right to eject from any event any Sponsor or representative violating those standards. + +1. [**Deadlines**]{.underline}. Company logos, descriptions, banners, advertising pages, tote bag inserts and similar items and information must be provided by the applicable deadlines for inclusion in the promotional materials for the PSF. + +1. [**Assignment of Space**]{.underline}. If the Sponsor Benefits in Exhibit A include a booth or other display space, the PSF shall assign display space to Sponsor for the period of the display. Location assignments will be on a first-come, first-served basis and will be made solely at the discretion of the PSF. Failure to use a reserved space will result in penalties (up to 50% of your Sponsorship Payment). + +1. [**Job Postings**]{.underline}. Sponsor will ensure that any job postings to be published by the PSF on Sponsor’s behalf comply with all applicable municipal, state, provincial, and federal laws. + +1. [**Representations and Warranties**]{.underline}. Each Party represents and warrants for the benefit of the other Party that it has the legal authority to enter into this Agreement and is able to comply with the terms herein. Sponsor represents and warrants for the benefit of the PSF that it has full right and title to the Sponsor Intellectual Property to be provided under this Agreement and is not under any obligation to any party that restricts the Sponsor Intellectual Property or would prevent Sponsor’s performance under this Agreement. + +1. [**Successors and Assigns**]{.underline}. This Agreement and all the terms and provisions hereof shall be binding upon and inure to the benefit of the Parties and their respective legal representatives, heirs, successors, and/or assigns. The transfer, or any attempted assignment or transfer, of all or any portion of this Agreement by a Party without the prior written consent of the other Party shall be null and void and of no effect. + +1. [**No Third-Party Beneficiaries**]{.underline}. This Agreement is not intended to benefit and shall not be construed to confer upon any person, other than the Parties, any rights, remedies, or other benefits, including but not limited to third-party beneficiary rights. + +1. [**Severability**]{.underline}. If any one or more of the provisions of this Agreement shall be held to be invalid, illegal, or unenforceable, the validity, legality, or enforceability of the remaining provisions of this Agreement shall not be affected thereby. To the extent permitted by applicable law, each Party waives any provision of law which renders any provision of this Agreement invalid, illegal, or unenforceable in any respect. + +1. [**Confidential Information**]{.underline}. As used herein, "Confidential Information" means all confidential information disclosed by a Party ("Disclosing Party") to the other Party ("Receiving Party"), whether orally or in writing, that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information. Each Party agrees: (a) to observe complete confidentiality with respect to the Confidential Information of the Disclosing Party; (b) not to disclose, or permit any third party or entity access to disclose, the Confidential Information (or any portion thereof) of the Disclosing Party without prior written permission of Disclosing Party; and (c) to ensure that any employees, or any third parties who receive access to the Confidential Information, are advised of the confidential and proprietary nature thereof and are prohibited from disclosing the Confidential Information and using the Confidential Information other than for the benefit of the Receiving Party in accordance with this Agreement. Without limiting the foregoing, each Party shall use the same degree of care that it uses to protect the confidentiality of its own confidential information of like kind, but in no event less than reasonable care. Neither Party shall have any liability with respect to Confidential Information to the extent such information: (w) is or becomes publicly available (other than through a breach of this Agreement); (x) is or becomes available to the Receiving Party on a non-confidential basis, provided that the source of such information was not known by the Receiving Party (after such inquiry as would be reasonable in the circumstances) to be the subject of a confidentiality agreement or other legal or contractual obligation of confidentiality with respect to such information; (y) is developed by the Receiving Party independently and without reference to information provided by the Disclosing Party; or (z) is required to be disclosed by law or court order, provided the Receiving Party gives the Disclosing Party prior notice of such compelled disclosure (to the extent legally permitted) and reasonable assistance, at the Disclosing Party’s cost. + +1. [**Independent Contractors**]{.underline}. Nothing contained herein shall constitute or be construed as the creation of any partnership, agency, or joint venture relationship between the Parties. Neither of the Parties shall have the right to obligate or bind the other Party in any manner whatsoever, and nothing herein contained shall give or is intended to give any rights of any kind to any third party. The relationship of the Parties shall be as independent contractors. + +1. [**Indemnification**]{.underline}. Sponsor agrees to indemnify and hold harmless the PSF, its officers, directors, employees, and agents, for any and all claims, losses, damages, liabilities, judgments, or settlements, including reasonable attorneys’ fees, costs (including costs associated with any official investigations or inquiries) and other expenses, incurred on account of Sponsor’s acts or omissions in connection with the performance of this Agreement or breach of this Agreement or with respect to the manufacture, marketing, sale, or dissemination of any of Sponsor’s products or services. The PSF shall have no liability to Sponsor with respect to its participation in this Agreement or receipt of the Sponsorship Payment, except for intentional or willful acts of the PSF or its employees or agents. The rights and responsibilities established in this section shall survive indefinitely beyond the term of this Agreement. + +1. [**Notices**]{.underline}. All notices or other communications to be given or delivered under the provisions of this Agreement shall be in writing and shall be mailed by certified or registered mail, return receipt requested, or given or delivered by reputable courier, facsimile, or electronic mail to the Party to receive notice at the following addresses or at such other address as any Party may by notice direct in accordance with this Section: + + + If to Sponsor: + + >          {{sponsor.primary_contact.name}} + >          {{sponsor.name}} + >          {{sponsor.mailing_address_line_1}}{%if sponsor.mailing_address_line_2%} + >          {{sponsor.mailing_address_line_2 }}{% endif %} + >          {{sponsor.city}}, {{sponsor.state}} {{sponsor.postal_code}} {{sponsor.country}} + >          Facsimile: {{sponsor.primary_contact.phone}} + >          Email: {{sponsor.primary_contact.email}} + +   + + If to the PSF: + + >          Deb Nicholson + >          Executive Director + >          Python Software Foundation + >          9450 SW Gemini Dr. ECM # 90772 + >          Beaverton, OR 97008 USA + >          Facsimile: +1 (858) 712-8966 + >          Email: deb@python.org + +   + + With a copy to: + + >          Archer & Greiner, P.C. + >          Attention: Noel Fleming + >          Three Logan Square + >          1717 Arch Street, Suite 3500 + >          Philadelphia, PA 19103 USA + >          Facsimile: (215) 963-9999 + >          Email: nfleming@archerlaw.com + +   + + Notices given by registered or certified mail shall be deemed as given on the delivery date shown on the return receipt, and notices given in any other manner shall be deemed as given when received. + +1. [**Governing Law; Jurisdiction**]{.underline}. This Agreement shall be construed in accordance with the laws of the State of Delaware, without regard to its conflicts of law principles. Jurisdiction and venue for litigation of any dispute, controversy, or claim arising out of or in connection with this Agreement shall be only in a United States federal court in Delaware or a Delaware state court having subject matter jurisdiction. Each of the Parties hereto hereby expressly submits to the personal jurisdiction of the foregoing courts located in Delaware and hereby waives any objection or defense based on personal jurisdiction or venue that might otherwise be asserted to proceedings in such courts. + +1. [**Force Majeure**]{.underline}. The PSF shall not be liable for any failure or delay in performing its obligations hereunder if such failure or delay is due in whole or in part to any cause beyond its reasonable control or the reasonable control of its contractors, agents, or suppliers, including, but not limited to, strikes, or other labor disturbances, acts of God, acts of war or terror, floods, sabotage, fire, natural, or other disasters, including pandemics. To the extent the PSF is unable to substantially perform hereunder due to any cause beyond its control as contemplated herein, it may terminate this Agreement as it may decide in its sole discretion. To the extent the PSF so terminates the Agreement, Sponsor releases the PSF and waives any claims for damages or compensation on account of such termination. + +1. [**No Waiver**]{.underline}. A waiver of any breach of any provision of this Agreement shall not be deemed a waiver of any repetition of such breach or in any manner affect any other terms of this Agreement. + +1. [**Limitation of Damages**]{.underline}. Except as otherwise provided herein, neither Party shall be liable to the other for any consequential, incidental, or punitive damages for any claims arising directly or indirectly out of this Agreement. + +1. [**Cumulative Remedies**]{.underline}. All rights and remedies provided in this Agreement are cumulative and not exclusive, and the exercise by either Party of any right or remedy does not preclude the exercise of any other rights or remedies that may now or subsequently be available at law, in equity, by statute, in any other agreement between the Parties, or otherwise. + +1. [**Captions**]{.underline}. The captions and headings are included herein for convenience and do not constitute a part of this Agreement. + +1. [**Amendments**]{.underline}. No addition to or change in the terms of this Agreement will be binding on any Party unless set forth in writing and executed by both Parties. + +1. [**Counterparts**]{.underline}. This Agreement may be executed in one or more counterparts, each of which shall be deemed an original and all of which shall be taken together and deemed to be one instrument. A signed copy of this Agreement delivered by facsimile, electronic mail, or other means of electronic transmission shall be deemed to have the same legal effect as delivery of an original signed copy of this Agreement. + +1. [**Entire Agreement**]{.underline}. This Agreement (including the Exhibits) sets forth the entire agreement of the Parties and supersedes all prior oral or written agreements or understandings between the Parties as to the subject matter of this Agreement. Except as otherwise expressly provided herein, neither Party is relying upon any warranties, representations, assurances, or inducements of the other Party. + +{% endif %} +  + + +### \[Signature Page Follows\] + +::: {.page-break} +\newpage +::: + +## SPONSORSHIP AGREEMENT{% if renewal %} RENEWAL{% endif %} + +**IN WITNESS WHEREOF**, the Parties hereto have duly executed this **Sponsorship Agreement{% if renewal %} Renewal{% endif %}** as of the **Effective Date**. + +  + +> **PSF**: +> **PYTHON SOFTWARE FOUNDATION**, +> a Delaware non profit corporation + +  + +> By:        ________________________________ +> Name:   Loren Crary +> Title:     Director of Resource Development + +  + +  + +> **SPONSOR**: +> **{{sponsor.name|upper}}**, +> a {{sponsor.state}} entity + +  + +> By:        ________________________________ +> Name:   ________________________________ +> Title:     ________________________________ + +::: {.page-break} +\newpage +::: + +## SPONSORSHIP AGREEMENT{% if renewal %} RENEWAL{% endif %} + +### EXHIBIT A + +1. [**Sponsorship**]{.underline}. During the Term of this Agreement, in return for the Sponsorship Payment, the PSF agrees to identify and acknowledge Sponsor as a {{sponsorship.year}} {{sponsorship.level_name}} Sponsor of the Programs and of the PSF, in accordance with the United States Internal Revenue Service guidance applicable to qualified sponsorship payments. + + Acknowledgments of appreciation for the Sponsorship Payment may identify and briefly describe Sponsor and its products or product lines in neutral terms and may include Sponsor’s name, logo, well-established slogan, locations, telephone numbers, or website addresses, but such acknowledgments shall not include (a) comparative or qualitative descriptions of Sponsor’s products, services, or facilities; (b) price information or other indications of savings or value associated with Sponsor’s products or services; (c) a call to action; (d) an endorsement; or (e) an inducement to buy, sell, or use Sponsor’s products or services. Any such acknowledgments will be created, or subject to prior review and approval, by the PSF. + + The PSF’s acknowledgment may include the following: + + (a) [**Display of Logo**]{.underline}. The PSF will display Sponsor’s logo and other agreed-upon identifying information on www.python.org, and on any marketing and promotional media made by the PSF in connection with the Programs, solely for the purpose of acknowledging Sponsor as a sponsor of the Programs in a manner (placement, form, content, etc.) reasonably determined by the PSF in its sole discretion. Sponsor agrees to provide all the necessary content and materials for use in connection with such display. + + (a) Additional acknowledgment as provided in Sponsor Benefits. + +1. [**Sponsorship Payment**]{.underline}. The amount of Sponsorship Payment shall be {{sponsorship.verbose_sponsorship_fee|title}} USD (${{sponsorship.sponsorship_fee|intcomma}}). The Sponsorship Payment is due within thirty (30) days of the Effective Date. To the extent that any portion of a payment under this section would not (if made as a Separate payment) be deemed a qualified sponsorship payment under IRC § 513(i), such portion shall be deemed and treated as separate from the qualified sponsorship payment. + +1. [**Receipt of Payment**]{.underline}. Sponsor must submit full payment in order to secure Sponsor Benefits. + +1. [**Refunds**]{.underline}. The PSF does not offer refunds for sponsorships. The PSF may cancel the event(s) or any part thereof. In that event, the PSF shall determine and refund to Sponsor the proportionate share of the balance of the aggregate Sponsorship fees applicable to event(s) received which remain after deducting all expenses incurred by the PSF. + +1. [**Sponsor Benefits**]{.underline}. Sponsor Benefits per the Agreement are: + + 1. Acknowledgement as described under "Sponsorship" above. + +{%for benefit in benefits%} 1. {{benefit}} +{%endfor%} + +{%if legal_clauses%}1. Legal Clauses. Related legal clauses are: + +{%for clause in legal_clauses%} 1. {{clause}} +{%endfor%}{%endif%} diff --git a/templates/sponsors/admin/preview-contract.html b/templates/sponsors/admin/preview-contract.html deleted file mode 100644 index f89fd02b0..000000000 --- a/templates/sponsors/admin/preview-contract.html +++ /dev/null @@ -1,283 +0,0 @@ -{% extends "easy_pdf/base.html" %} -{% load humanize %} - -{% block extra_style %} - -{% endblock %} - -{% block content %} -

SPONSORSHIP AGREEMENT

- -

THIS SPONSORSHIP AGREEMENT (the “Agreement”) is entered into and made -effective as of the {{ start_date|date:"dS" }} day of {{ start_date|date:"F, Y"}} (the “Effective Date”), by and between -Python Software Foundation (the “PSF”), a Delaware nonprofit corporation, and {{ sponsor.name|upper }} (“Sponsor”), a {{ sponsor.state }} corporation. Each of the PSF and Sponsor are -hereinafter sometimes individually referred to as a “Party” and collectively as the “Parties”.

- -

RECITALS

- -

WHEREAS, the PSF is a tax-exempt charitable organization (EIN 04-3594598) whose -mission is to promote, protect, and advance the Python programming language, and to support -and facilitate the growth of a diverse and international community of Python programmers (the -“Programs”);

- -

WHEREAS, Sponsor is {{ contract.sponsor_info}}; and

- -

WHEREAS, Sponsor wishes to support the Programs by making a contribution to the -PSF.

- -

AGREEMENT

- -

NOW, THEREFORE, in consideration of the foregoing and the mutual covenants -contained herein, and for other good and valuable consideration, the receipt and sufficiency of -which are hereby acknowledged, the Parties hereto agree as follows:

- -
    -
  1. Recitals Incorporated. Each of the above Recitals is incorporated into and is made a part of this Agreement.
  2. - -
  3. Exhibits Incorporated by Reference. All exhibits referenced in this Agreement are incorporated herein as integral parts of this Agreement and shall be considered reiterated herein as fully as if such provisions had been set forth verbatim in this Agreement.
  4. - -
  5. Sponsorship Payment. In consideration for the right to sponsor the PSF and its Programs, and to be acknowledged by the PSF as a sponsor in the manner described herein, Sponsor shall make a contribution to the PSF (the “Sponsorship Payment”) in the amount shown in Exhibit A.
  6. - -
  7. Acknowledgement of Sponsor. In return for the Sponsorship Payment, Sponsor will be entitled to receive the sponsorship package described in Exhibit A attached hereto (the “Sponsor Benefits”).
  8. - -
  9. Intellectual Property. The PSF is the sole owner of all right, title, and interest to all the PSF information, including the PSF’s logo, trademarks, trade names, and copyrighted information, unless otherwise provided.
  10. - -
      -
    1. Grant of License by the PSF. The PSF hereby grants to Sponsor a limited, non-exclusive license to use certain of the PSF’s intellectual property, including the PSF’s name, acronym, and logo (collectively, the “PSF Intellectual Property”), solely in connection with promotion of Sponsor’s sponsorship of the Programs. Sponsor agrees that it shall not use the PSF’s Property in a manner that states or implies that the PSF endorses Sponsor (or Sponsor’s products or services). The PSF retains the right, in its sole and absolute discretion, to review and approve in advance all uses of the PSF Intellectual Property, which approval shall not be unreasonably withheld.
    2. - -
    3. Grant of License by Sponsor. Sponsor hereby grants to the PSF a limited, non-exclusive license to use certain of Sponsor’s intellectual property, including names, trademarks, and copyrights (collectively, “Sponsor Intellectual Property”), solely to identify Sponsor as a sponsor of the Programs and the PSF. Sponsor retains the right to review and approve in advance all uses of the Sponsor Intellectual Property, which approval shall not be unreasonably withheld.
    4. -
    - - -
  11. Term. The Term of this Agreement will begin on {{ start_date|date:"dS, F Y"}} and continue for a period of one (1) year. The Agreement may be renewed for one (1) year by written notice from Sponsor to the PSF.
  12. - -
  13. Termination. The Agreement may be terminated (i) by either Party for any reason upon sixty (60) days prior written notice to the other Party; (ii) if one Party notifies the other Party that the other Party is in material breach of its obligations under this Agreement and such breach (if curable) is not cured with fifteen (15) days of such notice; (iii) if both Parties agree to terminate by mutual written consent; or (iv) if any of Sponsor information is found or is reasonably alleged to violate the rights of a third party. The PSF shall also have the unilateral right to terminate this Agreement at any time if it reasonably determines that it would be detrimental to the reputation and goodwill of the PSF or the Programs to continue to accept or use funds from Sponsor. Upon expiration or termination, no further use may be made by either Party of the other’s name, marks, logo or other intellectual property without the express prior written authorization of the other Party.
  14. - -
  15. Code of Conduct. Sponsor and all of its representatives shall conduct themselves at all times in accordance with the Python Software Foundation Code of Conduct (https://www.python.org/psf/codeofconduct) and/or the PyCon Code of Conduct (https://us.pycon.org/2021/about/code-of-conduct/), as applicable. The PSF reserves the right to eject from any event any Sponsor or representative violating those standards.
  16. - -
  17. Deadlines. Company logos, descriptions, banners, advertising pages, tote bag inserts and similar items and information must be provided by the applicable deadlines for inclusion in the promotional materials for the PSF.
  18. - -
  19. Assignment of Space. If the Sponsor Benefits in Exhibit A include a booth or other display space, the PSF shall assign display space to Sponsor for the period of the display. Location assignments will be on a first-come, first-served basis and will be made solely at the discretion of the PSF. Failure to use a reserved space will result in penalties (up to 50% of your Sponsorship Payment).
  20. - -
  21. Job Postings. Sponsor will ensure that any job postings to be published by the PSF on Sponsor’s behalf comply with all applicable municipal, state, provincial, and federal laws.
  22. - -
  23. Representations and Warranties. Each Party represents and warrants for the benefit of the other Party that it has the legal authority to enter into this Agreement and is able to comply with the terms herein. Sponsor represents and warrants for the benefit of the PSF that it has full right and title to the Sponsor Intellectual Property to be provided under this Agreement and is not under any obligation to any party that restricts the Sponsor Intellectual Property or would prevent Sponsor’s performance under this Agreement.
  24. - -
  25. Successors and Assigns. This Agreement and all the terms and provisions hereof shall be binding upon and inure to the benefit of the Parties and their respective legal representatives, heirs, successors, and/or assigns. The transfer, or any attempted assignment or transfer, of all or any portion of this Agreement by a Party without the prior written consent of the other Party shall be null and void and of no effect.
  26. - -
  27. No Third-Party Beneficiaries. This Agreement is not intended to benefit and shall not be construed to confer upon any person, other than the Parties, any rights, remedies, or other benefits, including but not limited to third-party beneficiary rights.
  28. - -
  29. Severability. If any one or more of the provisions of this Agreement shall be held to be invalid, illegal, or unenforceable, the validity, legality, or enforceability of the remaining provisions of this Agreement shall not be affected thereby. To the extent permitted by applicable law, each Party waives any provision of law which renders any provision of this Agreement invalid, illegal, or unenforceable in any respect.
  30. - -
  31. Confidential Information. As used herein, “Confidential Information” means all confidential information disclosed by a Party (“Disclosing Party”) to the other Party (“Receiving Party”), whether orally or in writing, that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information. Each Party agrees: (a) to observe complete confidentiality with respect to the Confidential Information of the Disclosing Party; (b) not to disclose, or permit any third party or entity access to disclose, the Confidential Information (or any portion thereof) of the Disclosing Party without prior written permission of Disclosing Party; and (c) to ensure that any employees, or any third parties who receive access to the Confidential Information, are advised of the confidential and proprietary nature thereof and are prohibited from disclosing the Confidential Information and using the Confidential Information other than for the benefit of the Receiving Party in accordance with this Agreement. Without limiting the foregoing, each Party shall use the same degree of care that it uses to protect the confidentiality of its own confidential information of like kind, but in no event less than reasonable care. Neither Party shall have any liability with respect to Confidential Information to the extent such information: (w) is or becomes publicly available (other than through a breach of this Agreement); (x) is or becomes available to the Receiving Party on a non-confidential basis, provided that the source of such information was not known by the Receiving Party (after such inquiry as would be reasonable in the circumstances) to be the subject of a confidentiality agreement or other legal or contractual obligation of confidentiality with respect to such information; (y) is developed by the Receiving Party independently and without reference to information provided by the Disclosing Party; or (z) is required to be disclosed by law or court order, provided the Receiving Party gives the Disclosing Party prior notice of such compelled disclosure (to the extent legally permitted) and reasonable assistance, at the Disclosing Party’s cost.
  32. - -
  33. Independent Contractors. Nothing contained herein shall constitute or be construed as the creation of any partnership, agency, or joint venture relationship between the Parties. Neither of the Parties shall have the right to obligate or bind the other Party in any manner whatsoever, and nothing herein contained shall give or is intended to give any rights of any kind to any third party. The relationship of the Parties shall be as independent contractors.
  34. - -
  35. Indemnification. Sponsor agrees to indemnify and hold harmless the PSF, its officers, directors, employees, and agents, for any and all claims, losses, damages, liabilities, judgments, or settlements, including reasonable attorneys’ fees, costs (including costs associated with any official investigations or inquiries) and other expenses, incurred on account of Sponsor’s acts or omissions in connection with the performance of this Agreement or breach of this Agreement or with respect to the manufacture, marketing, sale, or dissemination of any of Sponsor’s products or services. The PSF shall have no liability to Sponsor with respect to its participation in this Agreement or receipt of the Sponsorship Payment, except for intentional or willful acts of the PSF or its employees or agents. The rights and responsibilities established in this section shall survive indefinitely beyond the term of this Agreement.
  36. - -
  37. - Notices. All notices or other communications to be given or delivered under the provisions of this Agreement shall be in writing and shall be mailed by certified or registered mail, return receipt requested, or given or delivered by reputable courier, facsimile, or electronic mail to the Party to receive notice at the following addresses or at such other address as any Party may by notice direct in accordance with this Section: - -
    -
    -

    If to Sponsor:

    -
    -

    {{ sponsor.primary_contact.name }}

    -

    {{ sponsor.name }}

    -

    {{ sponsor.mailing_address_line_1 }}

    - {% if sponsor.mailing_address_line_2 %} -

    {{ sponsor.mailing_address_line_2 }}

    - {% endif %} -

    Facsimile: {{ sponsor.primary_contact.phone }}

    -

    Email: {{ sponsor.primary_contact.email }}

    -
    -

    If to the PSF:

    -
    -

    Ewa Jodlowska

    -

    Executive Director

    -

    Python Software Foundation

    -

    9450 SW Gemini Dr. ECM # 90772

    -

    Beaverton, OR 97008 USA

    -

    Facsimile: +1 (858) 712-8966

    -

    Email: ewa@python.org

    -
    -

    With a copy to:

    -

    Fleming Petenko Law

    -

    1800 John F. Kennedy Blvd

    -

    Suite 904

    -

    Philadelphia, PA 19103 USA

    -

    Facsimile: (267) 422-9864

    -

    Email: info@nonprofitlawllc.com

    -
    -
    -

    Notices given by registered or certified mail shall be deemed as given on the delivery date shown on the return receipt, and notices given in any other manner shall be deemed as given when received.

    -
  38. - -
  39. Governing Law; Jurisdiction. This Agreement shall be construed in accordance with the laws of the State of Delaware, without regard to its conflicts of law principles. Jurisdiction and venue for litigation of any dispute, controversy, or claim arising out of or in connection with this Agreement shall be only in a United States federal court in Delaware or a Delaware state court having subject matter jurisdiction. Each of the Parties hereto hereby expressly submits to the personal jurisdiction of the foregoing courts located in Delaware and hereby waives any objection or defense based on personal jurisdiction or venue that might otherwise be asserted to proceedings in such courts.
  40. - -
  41. Force Majeure. The PSF shall not be liable for any failure or delay in performing its obligations hereunder if such failure or delay is due in whole or in part to any cause beyond its reasonable control or the reasonable control of its contractors, agents, or suppliers, including, but not limited to, strikes, or other labor disturbances, acts of God, acts of war or terror, floods, sabotage, fire, natural, or other disasters, including pandemics. To the extent the PSF is unable to substantially perform hereunder due to any cause beyond its control as contemplated herein, it may terminate this Agreement as it may decide in its sole discretion. To the extent the PSF so terminates the Agreement, Sponsor releases the PSF and waives any claims for damages or compensation on account of such termination.
  42. - -
  43. No Waiver. A waiver of any breach of any provision of this Agreement shall not be deemed a waiver of any repetition of such breach or in any manner affect any other terms of this Agreement.
  44. - -
  45. Limitation of Damages. Except as otherwise provided herein, neither Party shall be liable to the other for any consequential, incidental, or punitive damages for any claims arising directly or indirectly out of this Agreement.
  46. - -
  47. Cumulative Remedies. All rights and remedies provided in this Agreement are cumulative and not exclusive, and the exercise by either Party of any right or remedy does not preclude the exercise of any other rights or remedies that may now or subsequently be available at law, in equity, by statute, in any other agreement between the Parties, or otherwise.
  48. - -
  49. Captions. The captions and headings are included herein for convenience and do not constitute a part of this Agreement.
  50. - -
  51. Amendments. No addition to or change in the terms of this Agreement will be binding on any Party unless set forth in writing and executed by both Parties.
  52. - -
  53. Counterparts. This Agreement may be executed in one or more counterparts, each of which shall be deemed an original and all of which shall be taken together and deemed to be one instrument. A signed copy of this Agreement delivered by facsimile, electronic mail, or other means of electronic transmission shall be deemed to have the same legal effect as delivery of an original signed copy of this Agreement.
  54. - -
  55. Entire Agreement. This Agreement (including the Exhibits) sets forth the entire agreement of the Parties and supersedes all prior oral or written agreements or understandings between the Parties as to the subject matter of this Agreement. Except as otherwise expressly provided herein, neither Party is relying upon any warranties, representations, assurances, or inducements of the other Party.
  56. -
- -

[Signature Page Follows]

-
- -

SPONSORSHIP AGREEMENT

- - -

IN WITNESS WHEREOF, the Parties hereto have duly executed this __________________ Agreement as of the Effective Date.

- -
-

PSF:

-

PYTHON SOFTWARE FOUNDATION,

-

a Delaware nonprofit corporation

- -
-
- -

By: - ___________________________________

-
-

Ewa Jodlowska

-

Executive Director

-
- -
-
- -

SPONSOR:

-

______________________________________,

-

a {{ sponsor.state }} entity.

-
-

By: ___________________________________

- -
-
- -

SPONSORSHIP AGREEMENT

- -

EXHIBIT A

- -
    -
  1. Sponsorship. During the Term of this Agreement, in return for the Sponsorship Payment, the PSF agrees to identify and acknowledge Sponsor as a {{ start_date|date:'Y' }} {{ sponsorship.level_name }} Sponsor of the Programs and of the PSF, in accordance with the United States Internal Revenue Service guidance applicable to qualified sponsorship payments.
  2. - -

    Acknowledgments of appreciation for the Sponsorship Payment may identify and briefly describe Sponsor and its products or product lines in neutral terms and may include Sponsor’s name, logo, well-established slogan, locations, telephone numbers, or website addresses, but such acknowledgments shall not include (a) comparative or qualitative descriptions of Sponsor’s products, services, or facilities; (b) price information or other indications of savings or value associated with Sponsor’s products or services; (c) a call to action; (d) an endorsement; or (e) an inducement to buy, sell, or use Sponsor’s products or services. Any such acknowledgments will be created, or subject to prior review and approval, by the PSF.

    - -

    The PSF’s acknowledgment may include the following:

    - -
      -
    1. Display of Logo. The PSF will display Sponsor’s logo and other agreed-upon identifying information on www.python.org, and on any marketing and promotional media made by the PSF in connection with the Programs, solely for the purpose of acknowledging Sponsor as a sponsor of the Programs in a manner (placement, form, content, etc.) reasonably determined by the PSF in its sole discretion. Sponsor agrees to provide all the necessary content and materials for use in connection with such display.
    2. - -
    3. [Other use or Acknowledgement.]
    4. -
    - -
  3. Sponsorship Payment. The amount of Sponsorship Payment shall be {{ sponsorship.verbose_sponsorship_fee|title }} USD ($ {{ sponsorship.sponsorship_fee|intcomma }}). The Sponsorship Payment is due within thirty (30) days of the Effective Date. To the extent that any portion of a payment under this section would not (if made as a Separate payment) be deemed a qualified sponsorship payment under IRC § 513(i), such portion shall be deemed and treated as separate from the qualified sponsorship payment.
  4. - -
  5. Receipt of Payment. Sponsor must submit full payment in order to secure Sponsor Benefits.
  6. - -
  7. Refunds. The PSF does not offer refunds for sponsorships. The PSF may cancel the event(s) or any part thereof. In that event, the PSF shall determine and refund to Sponsor the proportionate share of the balance of the aggregate Sponsorship fees applicable to event(s) received which remain after deducting all expenses incurred by the PSF.
  8. - -
  9. Sponsor Benefits. Sponsor Benefits per the Agreement are:
  10. - -
      -
    1. Acknowledgement as described under “Sponsorship” above.
    2. - {% for benefit in benefits %} -
    3. {{ benefit }}
    4. - {% endfor %} -
    - - - {% if legal_clauses %} -
  11. Legal Clauses. Related legal clauses are:
  12. -
      - {% for clause in legal_clauses %} -
    1. {{ clause }}
    2. - {% endfor %} -
    - {% endif %} -
- -{% endblock %} diff --git a/templates/sponsors/admin/renewal-contract-template.docx b/templates/sponsors/admin/renewal-contract-template.docx deleted file mode 100644 index 3e36801a3..000000000 Binary files a/templates/sponsors/admin/renewal-contract-template.docx and /dev/null differ diff --git a/templates/users/sponsor_select.html b/templates/users/sponsor_select.html new file mode 100644 index 000000000..7c8185b8b --- /dev/null +++ b/templates/users/sponsor_select.html @@ -0,0 +1,30 @@ +{% extends "users/base.html" %} +{% load humanize pipeline %} + +{% block head %} + {% stylesheet 'font-awesome' %} +{% endblock %} + +{% block page_title %} + Select Sponsor | {{ SITE_INFO.site_name }} +{% endblock %} + +{% block body_attributes %}class="psf signup default-page"{% endblock %} + + +{% block main-nav_attributes %}psf-navigation{% endblock %} + +{% block user_content %} +

Select Sponsor to edit

+ +
+ +{% endblock %} + +{% block javascript %} + {{ block.super }} +{% endblock %} diff --git a/texlive.packages b/texlive.packages new file mode 100644 index 000000000..fc8668ea0 --- /dev/null +++ b/texlive.packages @@ -0,0 +1,2 @@ +xcolor +etoolbox diff --git a/users/urls.py b/users/urls.py index e925838b5..3ca7eccb7 100644 --- a/users/urls.py +++ b/users/urls.py @@ -18,6 +18,11 @@ views.UpdateSponsorInfoView.as_view(), name="edit_sponsor_info", ), + path( + "sponsorships/sponsor/edit/", + views.edit_sponsor_info_implicit, + name="edit_sponsor_info_implicit", + ), path( "sponsorships//assets/", views.UpdateSponsorshipAssetsView.as_view(), diff --git a/users/views.py b/users/views.py index c56dbace4..23140853e 100644 --- a/users/views.py +++ b/users/views.py @@ -277,6 +277,18 @@ def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, "Sponsor info updated with success.") return self.request.path +@login_required(login_url=settings.LOGIN_URL) +def edit_sponsor_info_implicit(request): + sponsors = Sponsor.objects.filter(contacts__user=request.user).all() + if len(sponsors) == 0: + messages.add_message(request, messages.INFO, "No Sponsors associated with your user.") + return redirect('users:user_profile_edit') + elif len(sponsors) == 1: + return redirect('users:edit_sponsor_info', pk=sponsors[0].id) + else: + messages.add_message(request, messages.INFO, "Multiple Sponsors associated with your user.") + return render(request, 'users/sponsor_select.html', context={"sponsors": sponsors}) + @method_decorator(login_required(login_url=settings.LOGIN_URL), name="dispatch") class UpdateSponsorshipAssetsView(UpdateView):