├── .cico.pipeline ├── doc ├── requirements.txt ├── _templates │ └── fedocal-logo.html ├── _static │ ├── fedocal.png │ └── site.css ├── icons │ ├── fedocal_logo.xcf │ └── fedocal_favicon.xcf ├── api.rst ├── translations.rst ├── contributing.rst ├── index.rst ├── contributors.rst ├── deployment.rst ├── configuration.rst ├── Makefile └── development.rst ├── createdb ├── TODO ├── fedocal ├── templates │ ├── default │ │ ├── markdown.html │ │ ├── base_partial.html │ │ ├── api.html │ │ ├── locations.html │ │ ├── admin.html │ │ ├── clear_calendar.html │ │ ├── index.html │ │ ├── add_calendar.html │ │ ├── edit_calendar.html │ │ ├── upload_calendar.html │ │ ├── _formhelpers.html │ │ ├── delete_calendar.html │ │ ├── delete_meeting.html │ │ ├── my_meeting.html │ │ ├── master.html │ │ ├── view_meeting.html │ │ ├── edit_meeting.html │ │ └── add_meeting.html │ └── centos │ │ └── master.html ├── static │ ├── default │ │ ├── edit.png │ │ ├── home.png │ │ ├── ical.png │ │ ├── list.png │ │ ├── plus.png │ │ ├── table.png │ │ ├── Approved.png │ │ ├── Denied.png │ │ ├── calendar.png │ │ ├── delete.png │ │ ├── favicon.ico │ │ ├── fedocal.png │ │ ├── html-bg.png │ │ ├── header-bg.png │ │ ├── fedocal-logo.png │ │ ├── fedora-logo.png │ │ ├── calendar-small.png │ │ ├── fedocal │ │ │ └── images │ │ │ │ ├── ui-icons_3c6eb4_256x240.png │ │ │ │ ├── ui-icons_454545_256x240.png │ │ │ │ ├── ui-icons_666666_256x240.png │ │ │ │ ├── ui-icons_db3279_256x240.png │ │ │ │ ├── ui-icons_ffffff_256x240.png │ │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ │ ├── ui-bg_flat_0_eeeeee_40x100.png │ │ │ │ ├── ui-bg_flat_55_ffffff_40x100.png │ │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ │ ├── ui-bg_highlight-soft_100_f6f6f6_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_25_3c6eb4_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_50_dddddd_1x100.png │ │ │ │ └── ui-bg_highlight-soft_75_dddddd_1x100.png │ │ ├── jquery-ui-timespinner.js │ │ ├── fedocal_meeting_details.js │ │ └── jquery.countdown.min.js │ └── centos │ │ └── centocal-logo.png ├── fedocallib │ ├── exceptions.py │ ├── fedmsgshim.py │ ├── dbaction.py │ ├── week.py │ └── fedora_calendar.py ├── i18nforms.py ├── fedocal_babel.py ├── default_config.py ├── doc_utils.py └── mail_logging.py ├── babel.cfg ├── alembic_upgrade.sh ├── wsgi.py ├── createdb.py ├── .s2i └── environment ├── .gitignore ├── MANIFEST.in ├── tests ├── sample_files │ ├── meeting.ical │ └── meeting_with_reminder.ical ├── fedocal_test.cfg ├── client_secrets.json ├── ical.ics ├── ical_wrong.txt ├── test_reminder.py ├── __init__.py ├── test_week.py ├── test_fedora_calendar.py ├── test_calendar.py └── test_flask_extras.py ├── fedora-messaging.toml.example ├── fedocal.wsgi ├── alembic ├── script.py.mako ├── versions │ ├── 2e5d25a095df_add_boolean_to_meeti.py │ ├── 43bc10530adb_add_calendar_admin.py │ ├── 351329b8d7da_add_remind_from_fiel.py │ ├── 45d83da297e8_add_calendar_contact.py │ ├── 2c5c36431061_add_the_meeting_date.py │ ├── 3f249e0d2769_add_a_timezone_field.py │ ├── 4f509700da52_add_status_field_for.py │ ├── 322997a7a41b_add_calendar_status_table.py │ ├── 2a554683048e_rename_managers_as_e.py │ └── 4f8bd7cac829_clean_db.py ├── env.py └── update03_to_04.py ├── requirements.txt ├── tox.ini ├── fedocal.conf ├── alembic.ini.sample ├── update.sql ├── setup.py ├── runserver.py ├── fedocal.cfg.sample ├── README.rst └── fedocal_cron.py /.cico.pipeline: -------------------------------------------------------------------------------- 1 | fedoraInfraTox { } 2 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | cloud_sptheme 3 | -------------------------------------------------------------------------------- /createdb: -------------------------------------------------------------------------------- 1 | FEDOCAL_CONFIG=../fedocal.cfg python createdb.py 2 | -------------------------------------------------------------------------------- /doc/_templates/fedocal-logo.html: -------------------------------------------------------------------------------- 1 |

fedocal

2 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Unit-tests, unit-tests and more unit-tests 2 | 3 | - Complete the code documentation 4 | 5 | -------------------------------------------------------------------------------- /doc/_static/fedocal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/doc/_static/fedocal.png -------------------------------------------------------------------------------- /fedocal/templates/default/markdown.html: -------------------------------------------------------------------------------- 1 | {% autoescape off%} {{ content | markdown}} {% endautoescape %} 2 | -------------------------------------------------------------------------------- /doc/icons/fedocal_logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/doc/icons/fedocal_logo.xcf -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /doc/icons/fedocal_favicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/doc/icons/fedocal_favicon.xcf -------------------------------------------------------------------------------- /fedocal/static/default/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/edit.png -------------------------------------------------------------------------------- /fedocal/static/default/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/home.png -------------------------------------------------------------------------------- /fedocal/static/default/ical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/ical.png -------------------------------------------------------------------------------- /fedocal/static/default/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/list.png -------------------------------------------------------------------------------- /fedocal/static/default/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/plus.png -------------------------------------------------------------------------------- /fedocal/static/default/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/table.png -------------------------------------------------------------------------------- /fedocal/templates/default/base_partial.html: -------------------------------------------------------------------------------- 1 | {% block content %}{% endblock %} 2 | {% block jscripts %}{% endblock %} 3 | -------------------------------------------------------------------------------- /alembic_upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pip install alembic 4 | alembic -c /opt/app-root/config/alembic.ini upgrade head 5 | -------------------------------------------------------------------------------- /fedocal/static/default/Approved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/Approved.png -------------------------------------------------------------------------------- /fedocal/static/default/Denied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/Denied.png -------------------------------------------------------------------------------- /fedocal/static/default/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/calendar.png -------------------------------------------------------------------------------- /fedocal/static/default/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/delete.png -------------------------------------------------------------------------------- /fedocal/static/default/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/favicon.ico -------------------------------------------------------------------------------- /fedocal/static/default/fedocal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal.png -------------------------------------------------------------------------------- /fedocal/static/default/html-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/html-bg.png -------------------------------------------------------------------------------- /fedocal/static/default/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/header-bg.png -------------------------------------------------------------------------------- /fedocal/static/centos/centocal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/centos/centocal-logo.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal-logo.png -------------------------------------------------------------------------------- /fedocal/static/default/fedora-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedora-logo.png -------------------------------------------------------------------------------- /fedocal/static/default/calendar-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/calendar-small.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-icons_3c6eb4_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-icons_3c6eb4_256x240.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-icons_666666_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-icons_666666_256x240.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-icons_db3279_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-icons_db3279_256x240.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | The fedocal API 2 | =============== 3 | 4 | .. automethod:: fedocal.api.api 5 | 6 | .. automethod:: fedocal.api.api_calendars 7 | 8 | .. automethod:: fedocal.api.api_meetings 9 | -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_flat_0_eeeeee_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_flat_0_eeeeee_40x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_flat_55_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_flat_55_ffffff_40x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_highlight-soft_25_3c6eb4_1x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_highlight-soft_50_dddddd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_highlight-soft_50_dddddd_1x100.png -------------------------------------------------------------------------------- /fedocal/static/default/fedocal/images/ui-bg_highlight-soft_75_dddddd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedora-infra/fedocal/master/fedocal/static/default/fedocal/images/ui-bg_highlight-soft_75_dddddd_1x100.png -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | gunicorn_logger = logging.getLogger('gunicorn.error') 5 | gunicorn_logger.addHandler(logging.StreamHandler(sys.stdout)) 6 | 7 | from fedocal import APP as application 8 | -------------------------------------------------------------------------------- /createdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from fedocal import APP 4 | from fedocal.fedocallib import model 5 | 6 | model.create_tables( 7 | APP.config['DB_URL'], 8 | APP.config['PATH_ALEMBIC_INI'], 9 | True) 10 | -------------------------------------------------------------------------------- /.s2i/environment: -------------------------------------------------------------------------------- 1 | FEDOCAL_CONFIG=/opt/app-root/config/fedocal.cfg 2 | FEDORA_MESSAGING_CONF=/opt/app-root/config/fedora-messaging.toml 3 | UPGRADE_PIP_TO_LATEST=1 4 | LANG=en_US.UTF-8 5 | LC_ALL=en_US.UTF-8 6 | LC_CTYPE=en_US.UTF-8 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | *.swp 5 | *.mo 6 | *.cfg 7 | !babel.cfg 8 | dist 9 | *.egg-info 10 | build 11 | .project 12 | .pydevproject 13 | .coverage 14 | alembic.ini 15 | _build/ 16 | pylint.out 17 | nosetests.xml 18 | MANIFEST 19 | .tox 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst *.sample requirements.txt 2 | include fedocal.spec fedocal.wsgi fedocal.conf alembic.ini.sample 3 | include run_tests.sh createdb createdb.py nosetests 4 | recursive-include fedocal * 5 | recursive-include alembic * 6 | recursive-include tests * 7 | include doc/* 8 | 9 | -------------------------------------------------------------------------------- /tests/sample_files/meeting.ical: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN 4 | BEGIN:VEVENT 5 | UID:DUMMY_UID 6 | DTSTART:{start_datetime} 7 | DTEND:{end_datetime} 8 | DESCRIPTION:This is another test meeting 9 | ORGANIZER:pingou 10 | SUMMARY:test-meeting2 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /fedocal/templates/default/api.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {%block tag %}api{% endblock %} 5 | 6 | {% block title %}{{ _('API') }}{% endblock %} 7 | 8 | {% block content %} 9 | 10 | {{ api_html }} 11 | 12 | {{ calendars_api_html }} 13 | 14 | {{ meetings_api_html }} 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /doc/_static/site.css: -------------------------------------------------------------------------------- 1 | @import url("cloud.css"); 2 | @import url("http://fonts.googleapis.com/css?family=Comfortaa"); 3 | 4 | .fedocal-logo span { 5 | background: url("fedocal.png") no-repeat scroll 50% 0 transparent; 6 | display: block; 7 | width: 134px; 8 | height: 64px; 9 | margin-bottom: 10px; 10 | } 11 | 12 | h1.fedocal-logo { 13 | font-family: 'Comfortaa', sans-serif; 14 | margin-top: -10px; 15 | margin-bottom: 12px; 16 | } 17 | -------------------------------------------------------------------------------- /fedora-messaging.toml.example: -------------------------------------------------------------------------------- 1 | # Example configuraton for Fedora Messaging 2 | # More information on how to use it or what to do with it at: 3 | # https://fedora-messaging.readthedocs.io/en/stable/configuration.html 4 | 5 | # Broker address 6 | amqp_url = "amqp://" 7 | 8 | # Authentication is TLS-based 9 | [tls] 10 | ca_cert = "/etc/pki/tls/certs/ca-bundle.crt" 11 | keyfile = "/my/client/key.pem" 12 | certfile = "/my/client/cert.pem" 13 | 14 | [client_properties] 15 | app = "fedocal" -------------------------------------------------------------------------------- /fedocal.wsgi: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | #import os 4 | ## Set the environment variable pointing to the configuration file 5 | #os.environ['FEDOCAL_CONFIG'] = '/etc/fedocal/fedocal.cfg' 6 | 7 | ## The following is only needed if you did not install fedocal 8 | ## as a python module (for example if you run it from a git clone). 9 | #import sys 10 | #sys.path.insert(0, '/path/to/fedocal/') 11 | 12 | 13 | ## The most import line to make the wsgi working 14 | #from fedocal import APP as application 15 | -------------------------------------------------------------------------------- /tests/sample_files/meeting_with_reminder.ical: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN 4 | BEGIN:VEVENT 5 | UID:DUMMY_UID 6 | DTSTART:{start_datetime} 7 | DTEND:{end_datetime} 8 | DESCRIPTION:This is another test meeting 9 | ORGANIZER:pingou 10 | SUMMARY:test-meeting2 11 | BEGIN:VALARM 12 | ACTION:DISPLAY 13 | DESCRIPTION:[test_calendar] [Fedocal] Reminder meeting: test-meeting2 14 | TRIGGER:-PT1H 15 | END:VALARM 16 | END:VEVENT 17 | END:VCALENDAR 18 | -------------------------------------------------------------------------------- /tests/fedocal_test.cfg: -------------------------------------------------------------------------------- 1 | ### Secret key for the Flask application 2 | SECRET_KEY='' 3 | 4 | ### url to the database server: 5 | import os 6 | DB_URL = 'sqlite:///%s/test.db' % (os.path.dirname(os.path.abspath(__file__))) 7 | #DB_URL='sqlite:////tmp/fedocal_dev.sqlite' 8 | 9 | ### The FAS group in which the admin of fedocal are 10 | ADMIN_GROUP='packager' 11 | 12 | import os 13 | OIDC_CLIENT_SECRETS = os.path.join(os.path.dirname( 14 | os.path.abspath(__file__)), 'client_secrets.json') 15 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Used for when working from a virtualenv. 2 | # Use this file by running "$ pip install -r requirements.txt" 3 | alembic 4 | bleach 5 | email_validator 6 | flask-wtf 7 | flask 8 | flask-babel 9 | flask-oidc 10 | python-dateutil 11 | python-fedora>=0.3.33 12 | pytz>=2013b 13 | sqlalchemy 14 | vobject 15 | wtforms 16 | Markdown 17 | docutils 18 | psutil 19 | blinker # Required by flask >= 0.10 for the unit-tests 20 | flask_oidc 21 | fedora-messaging 22 | email_validator 23 | fedocal-messages>=1.5.0 24 | 25 | gunicorn 26 | psycopg2 27 | -------------------------------------------------------------------------------- /tests/client_secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "web": { 3 | "redirect_uris": ["http://localhost:5002/oidc_callback"], 4 | "token_uri": "https://iddev.fedorainfracloud.org/openidc/Token", 5 | "auth_uri": "https://iddev.fedorainfracloud.org/openidc/Authorization", 6 | "client_id": "client_id", 7 | "client_secret": "client_secret", 8 | "userinfo_uri": "https://iddev.fedorainfracloud.org/openidc/UserInfo", 9 | "token_introspection_uri": "https://iddev.fedorainfracloud.org/openidc/TokenInfo", 10 | "issuer": "https://iddev.fedorainfracloud.org/openidc/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /alembic/versions/2e5d25a095df_add_boolean_to_meeti.py: -------------------------------------------------------------------------------- 1 | """Add boolean to Meeting for full-day meeting 2 | 3 | Revision ID: 2e5d25a095df 4 | Revises: 45d83da297e8 5 | Create Date: 2012-12-12 08:46:15.378554 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2e5d25a095df' 11 | down_revision = '45d83da297e8' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column( 19 | 'meetings', 20 | sa.Column('full_day', sa.Boolean, default=False) 21 | ) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('meetings', 'full_day') 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3{9,10,12} 3 | # If the user is missing an interpreter, don't fail 4 | skip_missing_interpreters = True 5 | 6 | [testenv] 7 | usedevelop = True 8 | deps = 9 | -rrequirements.txt 10 | pytest 11 | pytest-cov 12 | install_command = 13 | pip install {opts} {packages} 14 | setenv = 15 | FEDOCAL_CONFIG={toxinidir}/tests/fedocal_test.cfg 16 | PYTHONPATH={toxinidir} 17 | passenv = HOME 18 | commands = 19 | pytest --cov=fedocal --cov-report=term-missing {posargs} 20 | 21 | 22 | [testenv:lint] 23 | deps = 24 | pylint 25 | commands = 26 | pylint -f parseable fedocal 27 | -------------------------------------------------------------------------------- /alembic/versions/43bc10530adb_add_calendar_admin.py: -------------------------------------------------------------------------------- 1 | """Add calendar admin 2 | 3 | Revision ID: 43bc10530adb 4 | Revises: 2e5d25a095df 5 | Create Date: 2013-05-27 18:17:12.891156 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '43bc10530adb' 11 | down_revision = '2e5d25a095df' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column( 19 | 'calendars', 20 | sa.Column('calendar_admin_group', sa.String(100), nullable=True) 21 | ) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('calendars', 'calendar_admin_group') 26 | -------------------------------------------------------------------------------- /fedocal.conf: -------------------------------------------------------------------------------- 1 | # Apache configuration file for fedocal 2 | 3 | #WSGIDaemonProcess fedocal user=apache maximum-requests=1000 display-name=fedocal processes=2 threads=1 4 | #WSGISocketPrefix run/wsgi 5 | #WSGIRestrictStdout On 6 | #WSGIRestrictSignal Off 7 | #WSGIPythonOptimize 1 8 | 9 | #WSGIScriptAlias /calendar /usr/share/fedocal/fedocal.wsgi 10 | 11 | # 12 | # WSGIProcessGroup fedocal 13 | # 14 | # # Apache 2.4 15 | # Require all granted 16 | # 17 | # 18 | # # Apache 2.2 19 | # Order deny,allow 20 | # Allow from all 21 | # 22 | # 23 | 24 | -------------------------------------------------------------------------------- /alembic/versions/351329b8d7da_add_remind_from_fiel.py: -------------------------------------------------------------------------------- 1 | """Add remind_from field 2 | 3 | Revision ID: 351329b8d7da 4 | Revises: 4f8bd7cac829 5 | Create Date: 2014-05-21 14:49:37.002055 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '351329b8d7da' 11 | down_revision = '4f8bd7cac829' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ''' Add the reminder_from column to the reminders table ''' 19 | op.add_column( 20 | 'reminders', 21 | sa.Column('reminder_from', sa.String(100)) 22 | ) 23 | 24 | 25 | def downgrade(): 26 | ''' Drop the reminder_from column to the reminders table ''' 27 | op.drop_column('reminders', 'reminder_from') 28 | -------------------------------------------------------------------------------- /doc/translations.rst: -------------------------------------------------------------------------------- 1 | Run following command in the same location where babel.cfg is located 2 | 3 | extract translatable strings from fedocal directory 4 | --------------- 5 | 6 | pybabel extract -F babel.cfg -k _l -o messages.pot fedocal 7 | 8 | 9 | Create new language translation, if it does not exist (here adding Finnish as an example) 10 | --------------- 11 | 12 | pybabel init -i messages.pot -d fedocal/translations -l fi 13 | 14 | 15 | Update existing translation strings for (any) languages 16 | --------------- 17 | 18 | pybabel update -i messages.pot -d fedocal/translations 19 | 20 | 21 | Compile existing translations (make the file used by the program itself) 22 | --------------- 23 | 24 | pybabel compile -d fedocal/translations 25 | -------------------------------------------------------------------------------- /alembic/versions/45d83da297e8_add_calendar_contact.py: -------------------------------------------------------------------------------- 1 | """Add calendar contact field 2 | 3 | Revision ID: 45d83da297e8 4 | Revises: 2c5c36431061 5 | Create Date: 2012-11-14 14:30:07.352865 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '45d83da297e8' 11 | down_revision = '2c5c36431061' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | """ Add the calendar_contact field to the calendar table. """ 19 | op.add_column( 20 | 'calendars', 21 | sa.Column('calendar_contact', sa.String(80)) 22 | ) 23 | 24 | 25 | def downgrade(): 26 | """ Remove the calendar_contact field to the calendar table. """ 27 | op.drop_column('calendars', 'calendar_contact') 28 | -------------------------------------------------------------------------------- /alembic/versions/2c5c36431061_add_the_meeting_date.py: -------------------------------------------------------------------------------- 1 | """Add the meeting_date_end field to the meetings table 2 | 3 | Revision ID: 2c5c36431061 4 | Revises: None 5 | Create Date: 2012-11-08 08:24:43.044403 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2c5c36431061' 11 | down_revision = None 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ''' Add the meeting_date_end column to the meetings table ''' 19 | op.add_column( 20 | 'meetings', 21 | sa.Column('meeting_date_end', sa.Date, 22 | default=sa.func.now() 23 | ) 24 | ) 25 | 26 | 27 | def downgrade(): 28 | ''' Drop the meeting_date_end column to the meetings table ''' 29 | op.drop_column('meetings', 'meeting_date_end') 30 | -------------------------------------------------------------------------------- /fedocal/templates/default/locations.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Home') }}{% endblock %} 4 | {%block tag %}locations{% endblock %} 5 | 6 | {% macro render_locations(calendars) -%} 7 | {% for location_row in locations %} 8 |
9 | {% for location in location_row %} 10 | 12 | {{ location }} 13 | 14 | {% endfor %} 15 |
16 | {% else %} 17 |

{{ _('No locations found') }}

18 | {% endfor %} 19 | {%- endmacro %} 20 | 21 | {% block content %} 22 |

{{ _('Locations') }}

23 |
24 | {{ render_locations(locations) }} 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /alembic/versions/3f249e0d2769_add_a_timezone_field.py: -------------------------------------------------------------------------------- 1 | """Add a timezone field 2 | 3 | Revision ID: 3f249e0d2769 4 | Revises: 2a554683048e 5 | Create Date: 2014-01-07 18:27:03.957521 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3f249e0d2769' 11 | down_revision = '2a554683048e' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column( 19 | 'meetings', 20 | sa.Column('meeting_timezone', sa.Text, default='UTC') 21 | ) 22 | ins = "UPDATE meetings SET meeting_timezone='UTC';" 23 | op.execute(ins) 24 | 25 | op.alter_column('meetings', 26 | column_name='meeting_timezone', 27 | nullable=False, 28 | existing_nullable=True, 29 | ) 30 | 31 | 32 | def downgrade(): 33 | op.drop_column('meetings', 'meeting_timezone') 34 | -------------------------------------------------------------------------------- /alembic/versions/4f509700da52_add_status_field_for.py: -------------------------------------------------------------------------------- 1 | """Add the calendar_status table 2 | 3 | Revision ID: 4f509700da52 4 | Revises: 2a554683048e 5 | Create Date: 2013-10-15 11:21:34.353817 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4f509700da52' 11 | down_revision = '43bc10530adb' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | status = sa.sql.table( 17 | 'calendar_status', 18 | sa.sql.column('status', sa.String(50)) 19 | ) 20 | 21 | 22 | def upgrade(): 23 | op.create_table( 24 | 'calendar_status', 25 | sa.Column('status', sa.String(50), primary_key=True), 26 | ) 27 | 28 | for str_status in ['Enabled', 'Disabled']: 29 | ins = "INSERT INTO calendar_status (status) VALUES ('%s');" % ( 30 | str_status) 31 | op.execute(ins) 32 | 33 | 34 | def downgrade(): 35 | op.drop_table('calendar_status') 36 | -------------------------------------------------------------------------------- /alembic/versions/322997a7a41b_add_calendar_status_table.py: -------------------------------------------------------------------------------- 1 | """Add status field for calendars 2 | 3 | Revision ID: 322997a7a41b 4 | Revises: 4f509700da52 5 | Create Date: 2013-10-15 11:52:16.185994 6 | 7 | """ 8 | from __future__ import print_function 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = '322997a7a41b' 12 | down_revision = '4f509700da52' 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | 17 | 18 | def upgrade(): 19 | try: 20 | op.add_column( 21 | 'calendars', 22 | sa.Column('calendar_status', 23 | sa.String(50), 24 | sa.ForeignKey('calendar_status.status'), 25 | nullable=False, 26 | server_default='Enabled') 27 | ) 28 | except Exception as e: 29 | print(e) 30 | 31 | 32 | def downgrade(): 33 | op.drop_column('calendars', 'calendar_status') 34 | -------------------------------------------------------------------------------- /fedocal/templates/default/admin.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Admin') }}{% endblock %} 4 | {%block tag %}admin{% endblock %} 5 | 6 | {% block content %} 7 |

{{ _('Admin interface') }}

8 | 9 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /tests/ical.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN 4 | 5 | BEGIN:VTIMEZONE 6 | 7 | TZID:CET 8 | BEGIN:STANDARD 9 | DTSTART:20001029T030000 10 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 11 | TZNAME:CET 12 | TZOFFSETFROM:+0200 13 | TZOFFSETTO:+0100 14 | END:STANDARD 15 | 16 | BEGIN:DAYLIGHT 17 | DTSTART:20000326T020000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 19 | TZNAME:CEST 20 | TZOFFSETFROM:+0100 21 | TZOFFSETTO:+0200 22 | END:DAYLIGHT 23 | 24 | END:VTIMEZONE 25 | 26 | BEGIN:VEVENT 27 | UID:20140114T123549Z-80634@carmine.pingoured.fr 28 | DTSTART;TZID=CET:20140113T030000 29 | DTEND;TZID=CET:20140113T040000 30 | DESCRIPTION: 31 | ORGANIZER:pingou\, 32 | SUMMARY:rec 33 | END:VEVENT 34 | 35 | BEGIN:VEVENT 36 | UID:20140114T123549Z-78075@carmine.pingoured.fr 37 | DTSTART;TZID=CET:20140707T020000 38 | DTEND;TZID=CET:20140707T030000 39 | DESCRIPTION: 40 | ORGANIZER:pingou\, 41 | SUMMARY:rec 42 | END:VEVENT 43 | END:VCALENDAR 44 | 45 | -------------------------------------------------------------------------------- /tests/ical_wrong.txt: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN 4 | 5 | BEGIN:VTIMEZONE 6 | 7 | TZID:CET 8 | BEGIN:STANDARD 9 | DTSTART:20001029T030000 10 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 11 | TZNAME:CET 12 | TZOFFSETFROM:+0200 13 | TZOFFSETTO:+0100 14 | END:STANDARD 15 | 16 | BEGIN:DAYLIGHT 17 | DTSTART:20000326T020000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 19 | TZNAME:CEST 20 | TZOFFSETFROM:+0100 21 | TZOFFSETTO:+0200 22 | END:DAYLIGHT 23 | 24 | END:VTIMEZONE 25 | 26 | BEGIN:VEVENT 27 | UID:20140114T123549Z-80634@carmine.pingoured.fr 28 | DTSTART;TZID=CET:20140113T030000 29 | DTEND;TZID=CET:20140113T040000 30 | DESCRIPTION: 31 | ORGANIZER:pingou\, 32 | SUMMARY:rec 33 | END:VEVENT 34 | 35 | BEGIN:VEVENT 36 | UID:20140114T123549Z-78075@carmine.pingoured.fr 37 | DTSTART;TZID=CET:20140707T020000 38 | DTEND;TZID=CET:20140707T030000 39 | DESCRIPTION: 40 | ORGANIZER:pingou\, 41 | SUMMARY:rec 42 | END:VEVENT 43 | END:VCALENDAR 44 | -------------------------------------------------------------------------------- /alembic/versions/2a554683048e_rename_managers_as_e.py: -------------------------------------------------------------------------------- 1 | """Rename managers as editors 2 | 3 | 4 | Revision ID: 2a554683048e 5 | Revises: 322997a7a41b 6 | Create Date: 2013-10-15 10:28:27.642713 7 | 8 | """ 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = '2a554683048e' 12 | down_revision = '322997a7a41b' 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | 17 | 18 | def upgrade(): 19 | op.alter_column('calendars', 20 | column_name='calendar_manager_group', 21 | name='calendar_editor_group', 22 | existing_type=sa.String(100), 23 | existing_nullable=True 24 | ) 25 | 26 | 27 | def downgrade(): 28 | op.alter_column('calendars', column_name='calendar_editor_group', 29 | name='calendar_manager_group', 30 | existing_type=sa.String(100), 31 | existing_nullable=True 32 | ) 33 | -------------------------------------------------------------------------------- /fedocal/static/default/jquery-ui-timespinner.js: -------------------------------------------------------------------------------- 1 | // define additional culture information for a possibly existing culture 2 | Globalize.addCultureInfo( "fedocal", { 3 | calendars: { 4 | standard: { 5 | patterns: { 6 | t: 'HH:mm' 7 | } 8 | } 9 | } 10 | }); 11 | Globalize.culture("fedocal"); 12 | 13 | $.widget( "ui.timespinner", $.ui.spinner, { 14 | options: { 15 | // half an hour 16 | step: 30 * 60 * 1000, 17 | // hours 18 | page: 60 19 | }, 20 | 21 | _parse: function( value ) { 22 | if ( typeof value === "string" ) { 23 | // already a timestamp 24 | if ( Number( value ) == value ) { 25 | return Number( value ); 26 | } 27 | return +Globalize.parseDate( value ); 28 | } 29 | return value; 30 | }, 31 | 32 | _format: function( value ) { 33 | return Globalize.format( new Date(value), "t" ); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /fedocal/templates/default/clear_calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Clear calendar') }}{% endblock %} 4 | 5 | {%block tag %}admin{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{{ _('Calendar:') }} {{ calendarobj.calendar_name}}

10 | 11 | 17 | 18 | 19 |

{{ _('Are you positively sure that\'s what you want to do?') }}

20 | 21 |
22 | {{ render_field_invert(form.confirm_delete) }} 23 |

24 | 25 | 26 |

27 | {{ form.csrf_token }} 28 |
29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /alembic.ini.sample: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = driver://user:pass@localhost/dbname 15 | 16 | 17 | # Logging configuration 18 | [loggers] 19 | keys = root,sqlalchemy,alembic 20 | 21 | [handlers] 22 | keys = console 23 | 24 | [formatters] 25 | keys = generic 26 | 27 | [logger_root] 28 | level = WARN 29 | handlers = console 30 | qualname = 31 | 32 | [logger_sqlalchemy] 33 | level = WARN 34 | handlers = 35 | qualname = sqlalchemy.engine 36 | 37 | [logger_alembic] 38 | level = INFO 39 | handlers = 40 | qualname = alembic 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /update.sql: -------------------------------------------------------------------------------- 1 | -- Sometime the database scheme from fedocal is changing 2 | -- This file tries to track down these changes allowing you to keep 3 | -- you database structure. 4 | -- However, this file is intended temporarily for development purposes 5 | -- until a better solution can be found (such as using Alembic). 6 | 7 | -- Use at your own risk -- Data may be lost 8 | 9 | USE fedocal; 10 | 11 | -- 2012/11/01 12 | ALTER TABLE `calendars` ADD calendar_regional_meetings tinyint(1) DEFAULT NULL; 13 | ALTER TABLE `meetings` ADD meeting_region varchar(10) DEFAULT NULL; 14 | 15 | -- 2012/11/05 - Change the Meetings table and drop the recursion table 16 | ALTER TABLE `meetings` ADD recursion_frequency Integer DEFAULT NULL; 17 | ALTER TABLE `meetings` ADD recursion_ends Date DEFAULT NULL; 18 | 19 | -- the foreign key name might change, see "SHOW CREATE TABLE meetings;" 20 | -- to be sure 21 | ALTER TABLE `meetings` DROP FOREIGN KEY `meetings_ibfk_3`; 22 | ALTER TABLE `meetings` DROP COLUMN `recursion_id`; 23 | 24 | DROP TABLE `recursivity`; 25 | 26 | 27 | -- Check the changes: 28 | SHOW TABLES ; 29 | SHOW CREATE TABLE meetings; 30 | DESCRIBE meetings; 31 | -------------------------------------------------------------------------------- /fedocal/templates/default/index.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Home') }}{% endblock %} 4 | {%block tag %}home{% endblock %} 5 | 6 | {% macro render_calendars(calendars) -%} 7 | {% for calendar_row in calendars %} 8 |
9 | {% for calendar in calendar_row %} 10 | 12 | 13 | {{ calendar.calendar_name }} 14 | {{ calendar.calendar_description }} 15 | 16 | {% endfor %} 17 |
18 | {% else %} 19 |

{{ _('No calendar found') }}

20 | {% endfor %} 21 | {%- endmacro %} 22 | 23 | {% block content %} 24 |

{{ _('Active Calendars') }}

25 |
26 | {{ render_calendars(calendars_table) }} 27 |
28 | 29 |

{{ _('Calendars disabled') }}

30 |
31 | {{ render_calendars(calendars_table2) }} 32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /fedocal/templates/default/add_calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | {% from "_formhelpers.html" import render_field_in_row %} 3 | 4 | {% block title %}{{ _('Add calendar') }}{% endblock %} 5 | 6 | {%block tag %}admin{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{{ _('New calendar') }}

12 |

{{ _('Please enter your calendar details in the form below.') }}

13 |
14 | 15 |
16 | 17 | {{ render_field_in_row(form.calendar_name) }} 18 | {{ render_field_in_row(form.calendar_contact) }} 19 | {{ render_field_in_row(form.calendar_description) }} 20 | {{ render_field_in_row(form.calendar_editor_groups) }} 21 | {{ render_field_in_row(form.calendar_admin_groups) }} 22 | {{ render_field_in_row(form.calendar_status) }} 23 |
24 |

25 | 26 | 27 | {{ form.csrf_token }} 28 |

29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /fedocal/fedocallib/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | exceptions - Different Exceptions classes used in the project. 5 | 6 | Copyright (C) 2012 Pierre-Yves Chibon 7 | Author: Pierre-Yves Chibon 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 3 of the License, or (at 12 | your option) any later version. 13 | See http://www.gnu.org/copyleft/gpl.html for the full text of the 14 | license. 15 | """ 16 | from __future__ import unicode_literals, absolute_import, print_function 17 | 18 | 19 | # pylint: disable=R0903 20 | class FedocalException(Exception): 21 | """ Exception thrown when a user is not allowed to perform a specific 22 | action. 23 | """ 24 | pass 25 | 26 | 27 | # pylint: disable=R0903 28 | class UserNotAllowed(FedocalException): 29 | """ Exception thrown when a user is not allowed to perform a specific 30 | action. 31 | """ 32 | pass 33 | 34 | 35 | # pylint: disable=R0903 36 | class InvalidMeeting(FedocalException): 37 | """ Exception thrown when a user is not allowed to perform a specific 38 | action. 39 | """ 40 | pass 41 | -------------------------------------------------------------------------------- /fedocal/templates/default/edit_calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | {% from "_formhelpers.html" import render_field_in_row %} 3 | 4 | {% block title %}{{ _('Edit calendar') }}{% endblock %} 5 | {%block tag %}admin{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{{ _('Edit calendar "%(name)s"', name=calendar.calendar_name) }}

11 |

{{ _('Edit the calendar details in the form below.') }}

12 |
13 | 14 |
15 | 16 | {{ render_field_in_row(form.calendar_name) }} 17 | {{ render_field_in_row(form.calendar_contact) }} 18 | {{ render_field_in_row(form.calendar_description) }} 19 | {{ render_field_in_row(form.calendar_editor_groups) }} 20 | {{ render_field_in_row(form.calendar_admin_groups) }} 21 | {{ render_field_in_row(form.calendar_status) }} 22 |
23 |

24 | 25 | 26 | {{ form.csrf_token }} 27 |

28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /fedocal/templates/default/upload_calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | {% from "_formhelpers.html" import render_field_in_row %} 3 | 4 | {% block title %}{{ _('Upload calendar') }}{% endblock %} 5 | 6 | {%block tag %}admin{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{{ _('Upload calendar') }}

12 |

13 | {{ _('Upload a calendar file using the %(ical_link)s format', 14 | ical_link='iCalendar') }} 15 |

16 |
17 | 18 |

{{ _('Note: recurrent events are not yet taken into account by the upload.') }}

19 |

{{ _('Note2: Todo are considered as full day meeting and displayed as such.') }}

20 | 21 |
22 | 23 | {{ render_field_in_row(form.ics_file) }} 24 |
25 |

26 | 27 | 28 | {{ form.csrf_token }} 29 |

30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /fedocal/fedocallib/fedmsgshim.py: -------------------------------------------------------------------------------- 1 | """ This is an utility module for sending notifications via fedora-messaging 2 | 3 | :Author: Ralph Bean 4 | :Author: Pierre-Yves Chibon 5 | 6 | """ 7 | from __future__ import unicode_literals, absolute_import 8 | 9 | import logging 10 | 11 | import fedora_messaging.api 12 | from fedora_messaging.exceptions import PublishReturned, ConnectionException 13 | 14 | import fedocal_messages 15 | import fedocal_messages.messages as schema 16 | 17 | 18 | _log = logging.getLogger(__name__) 19 | 20 | 21 | def publish(topic, msg): # pragma: no cover 22 | _log.debug('Publishing a message for %s: %s', topic, msg) 23 | try: 24 | 25 | msg_cls = fedocal_messages.get_message_object_from_topic( 26 | 'fedocal.%s' % topic 27 | ) 28 | 29 | if not hasattr(msg_cls, "app_name") is False: 30 | _log.warning( 31 | "fedocal is about to send a message that has no schemas: %s", 32 | topic 33 | ) 34 | 35 | message = msg_cls(body=msg) 36 | fedora_messaging.api.publish(message) 37 | _log.debug("Sent to fedora_messaging") 38 | except PublishReturned as e: 39 | _log.exception( 40 | 'Fedora Messaging broker rejected message %s: %s', 41 | message.id, e) 42 | except ConnectionException as e: 43 | _log.exception('Error sending message %s: %s', message.id, e) 44 | -------------------------------------------------------------------------------- /doc/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you're submitting patches to fedocal, please observe the following: 5 | 6 | - Check that your python code is `PEP8-compliant 7 | `_. There is a `pep8 tool 8 | `_ that can automatically check 9 | your source. 10 | 11 | - Check your code quality usint `pylint `_. 12 | The ``run_pylint.sh`` allows you to run pylint for the whole project which 13 | solves some problem that you might find if you run pylint on your file alone. 14 | 15 | - Check that your code doesn't break the test suite. The test suite can be 16 | run using the ``run_tests.sh`` shell script at the top of the sources. 17 | See :doc:`development` for more information about the test suite. 18 | 19 | - If you are adding new code, please write tests for them in ``fedocal/tests/``, 20 | the ``run_tests.sh`` script will help you to see the coverage of your code 21 | in unit-tests. 22 | 23 | - If your change warrants a modification to the docs in ``doc/`` or any 24 | docstrings in ``fedocal/`` please make that modification. 25 | 26 | .. note:: You have a doubt, you don't know how to do something, you have an 27 | idea but don't know how to implement it, you just have something bugging 28 | you? 29 | 30 | Come to see us on IRC: ``#fedora-apps`` on irc.freenode.net or via the 31 | `issue tracker of the project `_. 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Setup script 5 | """ 6 | 7 | import os 8 | import re 9 | 10 | from setuptools import setup 11 | 12 | 13 | here = os.path.abspath(os.path.dirname(__file__)) 14 | 15 | with open(os.path.join(here, "fedocal", "__init__.py")) as fd: 16 | match = re.search(r"^__version__ = '([^']+)'$", fd.read(), re.MULTILINE) 17 | VERSION = match.group(1) 18 | 19 | 20 | def get_requirements(requirements_file='requirements.txt'): 21 | """Get the contents of a file listing the requirements. 22 | 23 | :arg requirements_file: path to a requirements file 24 | :type requirements_file: string 25 | :returns: the list of requirements, or an empty list if 26 | `requirements_file` could not be opened or read 27 | :return type: list 28 | """ 29 | 30 | lines = open(requirements_file).readlines() 31 | 32 | return [ 33 | line.strip().split('#')[0] 34 | for line in lines 35 | if not line.startswith('#') 36 | ] 37 | 38 | 39 | setup( 40 | name='fedocal', 41 | description='fedocal is a web based calendar application for Fedora.', 42 | version=VERSION, 43 | author='Pierre-Yves Chibon', 44 | author_email='pingou@pingoured.fr', 45 | maintainer='Pierre-Yves Chibon', 46 | maintainer_email='pingou@pingoured.fr', 47 | license='GPLv3+', 48 | download_url='https://pagure.io/fedocal/releases/', 49 | url='https://pagure.io/fedocal/', 50 | packages=['fedocal'], 51 | include_package_data=True, 52 | scripts=['fedocal_cron.py'], 53 | install_requires=get_requirements(), 54 | ) 55 | -------------------------------------------------------------------------------- /fedocal/templates/default/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, after="") %} 2 |
3 | {{ field.label }}{{ field(**kwargs)|safe }} 4 | {% if field.flags.required %}*{% endif %} 5 | {% if after %} {{ after }}{% endif %} 6 | {% if field.errors %} 7 |
    8 | {% for error in field.errors %} 9 |
  • {{ error }}
  • 10 | {% endfor %} 11 |
12 | {% endif %} 13 |
14 | {% endmacro %} 15 | 16 | {% macro render_field_invert(field, after="") %} 17 |
18 | {{ field(**kwargs)|safe }}{{ field.label }} 19 | {% if field.flags.required %}*{% endif %} 20 | {% if after %} {{ after }}{% endif %} 21 | {% if field.errors %} 22 |
    23 | {% for error in field.errors %} 24 |
  • {{ error }}
  • 25 | {% endfor %} 26 |
27 | {% endif %} 28 |
29 | {% endmacro %} 30 | 31 | {% macro render_field_in_row(field, after="", escape_after=False) %} 32 | 33 | 34 | {{ field.label }} 35 | {% if field.flags.required %}*{% endif %} 36 | 37 | {{ field(**kwargs)|safe }} 38 | {% if escape_after %} 39 | {% autoescape off%} {% if after %} {{ after }}{% endif %} {% endautoescape %} 40 | {% else %} 41 | {% if after %} {{ after }}{% endif %} 42 | {% endif %} 43 | 44 | {% if field.errors %} 45 | 46 | {% for error in field.errors %} 47 | {{ error }} 48 | {% endfor %} 49 | 50 | {% endif %} 51 | {% endmacro %} 52 | -------------------------------------------------------------------------------- /alembic/versions/4f8bd7cac829_clean_db.py: -------------------------------------------------------------------------------- 1 | """Clean db 2 | 3 | Revision ID: 4f8bd7cac829 4 | Revises: 3f249e0d2769 5 | Create Date: 2014-01-09 14:03:13.997656 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4f8bd7cac829' 11 | down_revision = '3f249e0d2769' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ''' Drop the columns calendar_multiple_meetings and 19 | calendar_regional_meetings and rename meeting_region into 20 | meeting_location. 21 | ''' 22 | op.drop_column('calendars', 'calendar_multiple_meetings') 23 | op.drop_column('calendars', 'calendar_regional_meetings') 24 | op.alter_column( 25 | 'meetings', 26 | column_name='meeting_region', 27 | name='meeting_location', 28 | type_=sa.Text, 29 | existing_type=sa.String(100)) 30 | 31 | 32 | def downgrade(): 33 | ''' Add the columns calendar_multiple_meetings and 34 | calendar_regional_meetings and rename meeting_location into 35 | meeting_region. 36 | ''' 37 | op.add_column( 38 | 'calendars', 39 | sa.Column( 40 | 'calendar_multiple_meetings', 41 | sa.Boolean, default=False, 42 | nullable=False 43 | ) 44 | ) 45 | op.add_column( 46 | 'calendars', 47 | sa.Column( 48 | 'calendar_regional_meetings', 49 | sa.Boolean, default=False, 50 | nullable=False 51 | ) 52 | ) 53 | op.alter_column( 54 | 'meetings', 55 | column_name='meeting_location', 56 | name='meeting_region', 57 | type_=sa.String(100), 58 | existing_type=sa.Text) 59 | -------------------------------------------------------------------------------- /fedocal/templates/default/delete_calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Delete calendar') }}{% endblock %} 4 | 5 | {%block tag %}admin{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{{ _('Calendar:') }} {{ calendarobj.calendar_name}}

10 | 11 |
    12 |
  • {{ _('Contact:') }} {{ calendarobj.calendar_contact }}
  • 13 |
  • {{ _('Description:') }} {{ calendarobj.calendar_description }}
  • 14 |
  • {{ _('Manager group:') }} {{ calendarobj.calendar_editor_group }}
  • 15 |
  • {{ _('%(count)s meetings associated', count=calendarobj.meetings|length) }}
  • 16 |
17 | 18 |

{{ _("Are you positively sure that's what you want to do?") }}

19 | 20 |
21 | {{ render_field_invert(form.confirm_delete) }} 22 |

23 | 25 | 26 |

27 | {{ form.csrf_token }} 28 |
29 | 30 | {% endblock %} 31 | 32 | {% block jscripts %} 33 | {{ super() }} 34 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import argparse 4 | import sys 5 | import os 6 | 7 | 8 | parser = argparse.ArgumentParser( 9 | description='Run the fedocal app') 10 | parser.add_argument( 11 | '--config', '-c', dest='config', 12 | help='Configuration file to use for fedocal.') 13 | parser.add_argument( 14 | '--debug', dest='debug', action='store_true', 15 | default=False, 16 | help='Expand the level of data returned.') 17 | parser.add_argument( 18 | '--profile', dest='profile', action='store_true', 19 | default=False, 20 | help='Profile fedocal.') 21 | parser.add_argument( 22 | '--port', '-p', default=5000, 23 | help='Port for the fedocal to run on.') 24 | parser.add_argument( 25 | "--cert", "-s", default=None, help="Filename of SSL cert for the flask application." 26 | ) 27 | parser.add_argument( 28 | "--key", 29 | "-k", 30 | default=None, 31 | help="Filename of the SSL key for the flask application.", 32 | ) 33 | parser.add_argument( 34 | '--host', default="127.0.0.1", 35 | help='Hostname to listen on. When set to 0.0.0.0 the server is available ' 36 | 'externally. Defaults to 127.0.0.1 making it only visible on localhost') 37 | 38 | args = parser.parse_args() 39 | 40 | if args.config: 41 | config = args.config 42 | if not config.startswith('/'): 43 | here = os.path.join(os.path.dirname(os.path.abspath(__file__))) 44 | config = os.path.join(here, config) 45 | os.environ['FEDOCAL_CONFIG'] = config 46 | 47 | from fedocal import APP 48 | 49 | if args.profile: 50 | from werkzeug.contrib.profiler import ProfilerMiddleware 51 | APP.config['PROFILE'] = True 52 | APP.wsgi_app = ProfilerMiddleware(APP.wsgi_app, restrictions=[30]) 53 | 54 | APP.debug = True 55 | if args.cert and args.key: 56 | APP.run(host=args.host, port=int(args.port), ssl_context=(args.cert, args.key)) 57 | else: 58 | APP.run(host=args.host, port=int(args.port)) 59 | -------------------------------------------------------------------------------- /fedocal/i18nforms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | (c) 2014 - Copyright Johan Cwiklinski 5 | (c) 2014 - Copyright Patrick Uiterwijk 6 | 7 | Distributed under License GPLv3 or later 8 | You can find a copy of this license on the website 9 | http://www.gnu.org/licenses/gpl.html 10 | 11 | This program is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program; if not, write to the Free Software 23 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 24 | MA 02110-1301, USA. 25 | """ 26 | from __future__ import unicode_literals, absolute_import, print_function 27 | 28 | try: 29 | from flask_wtf import FlaskForm as FlaskForm 30 | except ImportError: 31 | from flask_wtf import Form as FlaskForm 32 | 33 | from fedocal.fedocal_babel import gettext, ngettext 34 | 35 | 36 | class Translations(object): 37 | """Translations object (see 38 | http://wtforms.readthedocs.org/en/1.0.5/i18n.html#writing-your-own-translations-provider 39 | """ 40 | def __init__(self): 41 | """ Constructor instanciates the gettext and ngettext methods used 42 | by the forms to translate. 43 | """ 44 | self.gettext = gettext 45 | self.ngettext = ngettext 46 | 47 | 48 | class Form(FlaskForm): 49 | """I18n form""" 50 | def _get_translations(self): 51 | """I18n form translation""" 52 | return Translations() 53 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Fedocal 2 | ======= 3 | 4 | Fedocal is a web based calendar application for Fedora. 5 | 6 | 7 | A number of groups in Fedora are using a calendar to coordinate their work, 8 | and at the moment the only tool at their disposition is the 9 | `wiki `_ with all the challenge of properly 10 | formatting tables in the mediawiki syntax. 11 | 12 | 13 | This project aims at providing a single place where these different calendars 14 | can be found and managed. This should save us from wiki page such as 15 | the `fedora-meeting irc calendar 16 | `_ or the `ambassador event 17 | `_ page 18 | which are either hardly maintained or plain un-readable. 19 | 20 | 21 | Additionally, a number of groups are sending reminders for their meeting, this 22 | is true for the regional ambassadors, the board, and the infrastructure 23 | meetings. This application aims at automating this process in order to take 24 | this task off the shoulders of fellow contributors. 25 | 26 | Resources: 27 | 28 | - `Home page `_ 29 | - `Documentation `_ 30 | - `Git repository `_ 31 | - `Github mirror `_ 32 | - `Discussion mailing-list 33 | `_ 34 | - `Developer mailing-list 35 | `_ (this 36 | contains changes in the trac as well as commit notifications. It is not 37 | intended for discussion) 38 | 39 | 40 | Contents: 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | 45 | deployment 46 | configuration 47 | usage 48 | api 49 | development 50 | contributing 51 | contributors 52 | 53 | 54 | 55 | Indices and tables 56 | ================== 57 | 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | * :ref:`search` 61 | 62 | -------------------------------------------------------------------------------- /doc/contributors.rst: -------------------------------------------------------------------------------- 1 | Contributors to fedocal 2 | ======================= 3 | 4 | Fedocal would be nothing without its contributors. 5 | 6 | On July 24, 2015 (release 0.16), the list looks as follow: 7 | 8 | ================= =========== 9 | Number of commits Contributor 10 | ================= =========== 11 | 1620 Pierre-Yves Chibon 12 | 113 Johan Cwiklinski 13 | 17 Toshio Kuratomi 14 | 15 Ratnadeep Debnath 15 | 13 Ralph Bean 16 | 3 Haikel Guemar 17 | 3 Maciej Lasyk 18 | 3 rtnpro 19 | 2 Amitosh Swain Mahapatra 20 | 2 Patrick Uiterwijk 21 | 2 Robert Mayr 22 | 1 Asutosh989 23 | 1 Cristian Ciupitu 24 | 1 Haikel Guemar 25 | 1 Jonathan Wakely 26 | 1 Lukas Brabec 27 | 1 Paul W. Frields 28 | 1 Toshio Kuratomi 29 | 1 Viorel Tabara 30 | 1 sumantrom 31 | ================= =========== 32 | 33 | This list is generated using 34 | 35 | :: 36 | 37 | git shortlog -s -n -e 38 | 39 | The fedocal logo is the results of the work of: Mark Klein (aka areke). 40 | 41 | The logos used in the sidebard on the calendar view and meetings list view 42 | originate from the `iconmoon `_ project with modifications 43 | by the fedocal authors. 44 | At the time of their inclusion, these logos were licensed under these two 45 | licenses: CC BY 3.0 or GPL. 46 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | target_metadata = None 19 | 20 | # other values from the config, defined by the needs of env.py, 21 | # can be acquired: 22 | # my_important_option = config.get_main_option("my_important_option") 23 | # ... etc. 24 | 25 | 26 | def run_migrations_offline(): 27 | """Run migrations in 'offline' mode. 28 | 29 | This configures the context with just a URL 30 | and not an Engine, though an Engine is acceptable 31 | here as well. By skipping the Engine creation 32 | we don't even need a DBAPI to be available. 33 | 34 | Calls to context.execute() here emit the given string to the 35 | script output. 36 | 37 | """ 38 | url = config.get_main_option("sqlalchemy.url") 39 | context.configure(url=url) 40 | 41 | with context.begin_transaction(): 42 | context.run_migrations() 43 | 44 | 45 | def run_migrations_online(): 46 | """Run migrations in 'online' mode. 47 | 48 | In this scenario we need to create an Engine 49 | and associate a connection with the context. 50 | 51 | """ 52 | engine = engine_from_config( 53 | config.get_section(config.config_ini_section), 54 | prefix='sqlalchemy.', 55 | poolclass=pool.NullPool) 56 | 57 | connection = engine.connect() 58 | context.configure( 59 | connection=connection, 60 | target_metadata=target_metadata 61 | ) 62 | 63 | try: 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | finally: 67 | connection.close() 68 | 69 | 70 | if context.is_offline_mode(): 71 | run_migrations_offline() 72 | else: 73 | run_migrations_online() 74 | -------------------------------------------------------------------------------- /fedocal/static/default/fedocal_meeting_details.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var _date = FEDOCAL.NEXT_MEETING.startDate; 3 | $('#countdown').countdown( 4 | new Date(_date), 5 | function(event) { 6 | $(this).html( 7 | event.strftime( 8 | FEDOCAL.MESSAGES.NEXT_MEETING_IN 9 | ) 10 | ); 11 | } 12 | ).on('finish.countdown', function(event){ 13 | var _end_date = FEDOCAL.NEXT_MEETING.endDate; 14 | var _current_date = new Date(); 15 | _current_date = Date.UTC( 16 | _current_date.getUTCFullYear(), 17 | _current_date.getUTCMonth(), 18 | _current_date.getUTCDate(), 19 | _current_date.getUTCHours(), 20 | _current_date.getUTCMinutes(), 21 | 0 22 | ); 23 | 24 | if ( _end_date > _current_date ) { 25 | $(this).html('

' + FEDOCAL.MESSAGES.MEETING_IN_PROGRESS + '

'); 26 | } else { 27 | $(this).html('

' + FEDOCAL.MESSAGES.MEETING_IS_OVER + '

'); 28 | } 29 | }); 30 | 31 | // Handle iCal meeting export with reminder toggle 32 | $('#ical-meeting-export-reminder-toggle').on('change', function (e) { 33 | var $icalExportLink = $('#ical-meeting-export'); 34 | var $icalReminderOptions = $('#ical-meeting-export-reminder-at'); 35 | if (this.checked) { 36 | $icalReminderOptions.removeAttr('disabled'); 37 | $icalExportLink.attr( 38 | 'href', $icalExportLink.attr('href') + '?reminder_delta=' + 39 | $icalReminderOptions.val()); 40 | } else { 41 | $icalReminderOptions.attr('disabled', 'disabled'); 42 | $icalExportLink.attr( 43 | 'href', $icalExportLink.attr('href').split('?')[0]); 44 | } 45 | }); 46 | 47 | // Handle change event in reminder options dropdown in iCal export 48 | $('#ical-meeting-export-reminder-at').on('change', function (e) { 49 | var $this = $(this); 50 | var $icalExportLink = $('#ical-meeting-export'); 51 | $icalExportLink.attr( 52 | 'href', 53 | $icalExportLink.attr('href').split('?')[0] + 54 | '?reminder_delta=' + $this.val()); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /fedocal.cfg.sample: -------------------------------------------------------------------------------- 1 | # Beware that the quotes around the values are mandatory 2 | 3 | ### Secret key for the Flask application 4 | SECRET_KEY='' 5 | 6 | ### url to the database server: 7 | #DB_URL=mysql://user:pass@host/db_name 8 | #DB_URL=postgres://user:pass@host/db_name 9 | DB_URL='sqlite:////var/tmp/fedocal_dev.sqlite' 10 | 11 | ### The FAS group in which the admin of fedocal are 12 | ADMIN_GROUP='fedocal_admin' 13 | 14 | ### The address of the SMTP server used to send the reminders emails 15 | ### via the cron job. 16 | ### Default: 'localhost' 17 | SMTP_SERVER='localhost' 18 | 19 | ### The cron job can be set with any frequency but fedocal_cron needs 20 | ### this information to accurately retrieve the meetings to remind and 21 | ### avoid sending multiple reminders for a single meeting. 22 | ### Default: 30 23 | CRON_FREQUENCY=30 24 | 25 | ### Path to the alembic configuration file 26 | ### When creating the database, we need to tell the database which 27 | ### revision of alembic we are at and to do this we need access to the 28 | ### alembic.ini file. 29 | PATH_ALEMBIC_INI='/etc/fedocal/alembic.ini' 30 | 31 | ### The URL at which the project is available. 32 | ### This is used in the reminder email to give the appropriate link 33 | ### This is also used by flask to generate link with the proper protocol 34 | ### (http/https) 35 | SITE_ROOT = 'https://apps.fedoraproject.org' 36 | SITE_URL = '%s/calendar' % SITE_ROOT 37 | 38 | ### Optional Configuration 39 | 40 | 41 | ### Name of the desired theme for fedocal. 42 | ### 43 | ### These themes must be located in the traditional 'templates' and 'static' 44 | ### folder used by fedocal, but assuming you have your own templates and 45 | ### static files, you can switch from one to another here. 46 | ### It defaults to 'default' which is the name of the default theme shipped 47 | ### with fedocal. 48 | # THEME_FOLDER = 'default' 49 | 50 | ### File extensions and mimetypes allowed for the upload 51 | ### When uploading a iCalendar file, fedocal checks the file extension as well 52 | ### as the mimetype of the file submitted, the file extensions and the mimetypes 53 | ### are listed here. If the file uploaded does not have an extension or a 54 | ### mimetype among those listed here, the upload will be refused. 55 | # ALLOWED_EXTENSIONS = ['ics', 'ical', 'ifb', 'icalendar'] 56 | # ALLOWED_MIMETYPES = [ 57 | # 'text/calendar', 58 | # ] 59 | 60 | ### The email address to which the flask.log will send the errors 61 | ### (tracebacks). 62 | ### In case of exception flask sends an email with some information to debug the 63 | ### problem. The email is sent to the address listed here. 64 | # EMAIL_ERROR = 'pingou@pingoured.fr' 65 | 66 | ### Options for iCal remind before dropdown 67 | ICAL_REMINDER_OPTIONS = ( 68 | ('5', '5 minutes'), 69 | ('60', '1 hour'), 70 | ('1440', '1 day') 71 | ) 72 | 73 | -------------------------------------------------------------------------------- /fedocal/templates/default/delete_meeting.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Delete meeting') }}{% endblock %} 4 | 5 | {%block tag %}mymeeting{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{{ _('Meeting:') }} {{ meeting.meeting_name}}

10 | 11 |
    12 |
  • {{ _('Date:') }} {{ next_meeting.meeting_date }}
  • 13 |
  • {{ _('Starting time:') }} {{ next_meeting.meeting_time_start }} {{ 14 | next_meeting.meeting_timezone }}
  • 15 |
  • {{ _('Ending time:') }} {{ next_meeting.meeting_time_stop }} {{ 16 | next_meeting.meeting_timezone }}
  • 17 |
18 | 19 |

{{ _('Stored as:') }}

20 |
    21 |
  • 22 | {{ _('Start:') }} {{ meeting.meeting_date }} - {{ meeting.meeting_time_start 23 | }} {{ meeting.meeting_timezone }}
  • 24 |
  • 25 | {{ _('End:') }} {{ meeting.meeting_date_end }} - {{ meeting.meeting_time_stop 26 | }} {{ meeting.meeting_timezone }}
  • 27 |
28 | 29 |

30 | {{ _('You are removing this meeting from the calendar') }} 31 | 32 | {{ meeting.calendar_name }}. 33 |

34 | 35 |

{{ _("Are you positively sure that's what you want to do?") }}

36 | 37 |
38 | {{ render_field_invert(form.confirm_delete) }} 39 | {% if meeting.recursion_frequency %} 40 |

{{ _('This meeting is a regular meeting, are you sure you want to delete all 41 | of them or just this one ? (Check only if you want to delete all future 42 | meetings as well)') }} 43 |

44 |

45 | {{ _('This meeting is recurrent, it occurs every %(frequency)s days until %(ends)s', frequency=meeting.recursion_frequency, ends=meeting.recursion_ends) }} 46 |

47 | {{ render_field_invert(form.confirm_futher_delete) }} 48 | {% endif %} 49 |

50 | 52 | 54 |

55 | {{ form.csrf_token }} 56 |
57 | 58 | {% endblock %} 59 | 60 | {% block jscripts %} 61 | {{ super() }} 62 | 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /fedocal/fedocal_babel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | (c) 2014 - Copyright Johan Cwiklinski 5 | (c) 2014 - Copyright Patrick Uiterwijk 6 | (c) 2014-2017 - Copyright Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | """ 27 | from __future__ import unicode_literals, absolute_import, print_function 28 | 29 | from babel import support 30 | 31 | try: 32 | from flask_babel import ( 33 | Babel, lazy_gettext, gettext, ngettext, format_datetime, get_locale) 34 | except ImportError: 35 | 36 | def gettext(string, **variables): 37 | """Wrapper for gettext functions, if flask-babel is missing""" 38 | try: 39 | return string % variables 40 | except KeyError: 41 | return string 42 | 43 | def ngettext(singular, plural, num, **variables): 44 | """Wrapper for ngettext functions, if flask-babel is missing""" 45 | variables['num'] = num 46 | if num == 1: 47 | return gettext(singular, **variables) 48 | else: 49 | return gettext(plural, **variables) 50 | 51 | def lazy_gettext(string, **variables): 52 | """Wrapper for lazy_gettext function, if flask-babel is missing""" 53 | try: 54 | return string % variables 55 | except KeyError: 56 | return string 57 | 58 | def format_datetime(datetime=None, format=None, rebase=True): 59 | """Wrapper for format_datetime function, if flask-babel is missing""" 60 | return datetime 61 | 62 | def get_locale(): 63 | """Wrapper for get_locale, if flask-babel is missing""" 64 | return 'en' 65 | 66 | class Babel(object): 67 | """Wrapper for Babel class, if flask-babel is missing""" 68 | 69 | def __init__(self, app): 70 | app.jinja_env.add_extension('jinja2.ext.i18n') 71 | app.jinja_env.install_gettext_callables( 72 | gettext, 73 | ngettext, 74 | newstyle=True 75 | ) 76 | 77 | def localeselector(self, func): 78 | return func 79 | -------------------------------------------------------------------------------- /fedocal/default_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | (c) 2012-2014 - Copyright Pierre-Yves Chibon 5 | 6 | Distributed under License GPLv3 or later 7 | You can find a copy of this license on the website 8 | http://www.gnu.org/licenses/gpl.html 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | MA 02110-1301, USA. 24 | """ 25 | from __future__ import unicode_literals, absolute_import 26 | 27 | import os 28 | 29 | from datetime import timedelta 30 | 31 | # Set the time after which the session expires 32 | PERMANENT_SESSION_LIFETIME = timedelta(hours=1) 33 | 34 | # url to the database server: 35 | DB_URL = 'sqlite:////var/tmp/fedocal_dev.sqlite' 36 | 37 | 38 | # The FAS group in which the admin of fedocal are 39 | ADMIN_GROUP = 'fedocal_admin' 40 | 41 | 42 | # The address of the SMTP server used to send the reminders emails 43 | # via the cron job. 44 | SMTP_SERVER = 'localhost' 45 | 46 | 47 | # The cron job can be set with any frequency but fedocal_cron 48 | CRON_FREQUENCY = 30 49 | 50 | # Path to the alembic configuration file 51 | PATH_ALEMBIC_INI = os.path.join( 52 | os.path.dirname(os.path.abspath(__file__)), 53 | '..', 'alembic.ini') 54 | 55 | # Name of the folder containing the desired theme inside the traditional 56 | # template folder. Defaults to 'default'. 57 | THEME_FOLDER = 'default' 58 | 59 | ALLOWED_EXTENSIONS = ['ics', 'ical', 'ifb', 'icalendar'] 60 | ALLOWED_MIMETYPES = [ 61 | 'text/calendar', 62 | ] 63 | 64 | # The email address to which the flask.log will send the errors (tracebacks) 65 | EMAIL_ERROR = 'pingou@pingoured.fr' 66 | 67 | # The URL at which the project is available. 68 | SITE_ROOT = 'https://apps.fedoraproject.org' 69 | SITE_URL = '%s/calendar' % SITE_ROOT 70 | 71 | LANGUAGES = { 72 | 'en': 'English', 73 | 'fr': 'Français' 74 | } 75 | 76 | # Options for iCal remind before dropdown 77 | ICAL_REMINDER_OPTIONS = ( 78 | ('5', '5 minutes'), 79 | ('60', '1 hour'), 80 | ('1440', '1 day') 81 | ) 82 | 83 | 84 | 85 | OIDC_CLIENT_SECRETS = os.path.join(os.path.dirname( 86 | os.path.abspath(__file__)), '..', 'client_secrets.json') 87 | OIDC_ID_TOKEN_COOKIE_SECURE = False 88 | OIDC_SCOPES = [ 89 | 'openid', 'email', 'profile', 90 | 'https://id.fedoraproject.org/scope/cla', 91 | 'https://id.fedoraproject.org/scope/groups', 92 | ] 93 | -------------------------------------------------------------------------------- /fedocal/doc_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | (c) 2013-2014 - Copyright Pierre-Yves Chibon 5 | 6 | Distributed under License GPLv3 or later 7 | You can find a copy of this license on the website 8 | http://www.gnu.org/licenses/gpl.html 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | MA 02110-1301, USA. 24 | """ 25 | 26 | from __future__ import unicode_literals, absolute_import 27 | 28 | import docutils 29 | import docutils.examples 30 | import markupsafe 31 | import six 32 | 33 | 34 | def modify_rst(rst): 35 | """ Downgrade some of our rst directives if docutils is too old. """ 36 | 37 | try: 38 | # The rst features we need were introduced in this version 39 | minimum = [0, 9] 40 | version = [int(el) for el in docutils.__version__.split('.')] 41 | 42 | # If we're at or later than that version, no need to downgrade 43 | if version >= minimum: 44 | return rst 45 | except Exception: # pragma: no cover 46 | # If there was some error parsing or comparing versions, run the 47 | # substitutions just to be safe. 48 | pass 49 | 50 | # On Fedora this will never work as the docutils version is to recent 51 | # Otherwise, make code-blocks into just literal blocks. 52 | substitutions = { # pragma: no cover 53 | '.. code-block:: javascript': '::', 54 | } 55 | for old, new in substitutions.items(): # pragma: no cover 56 | rst = rst.replace(old, new) 57 | 58 | return rst # pragma: no cover 59 | 60 | 61 | def modify_html(html): 62 | """ Perform style substitutions where docutils doesn't do what we want. 63 | """ 64 | 65 | substitutions = { 66 | '': '', 67 | '': '', 68 | } 69 | for old, new in substitutions.items(): 70 | html = html.replace(old, new) 71 | 72 | return html 73 | 74 | 75 | def load_doc(endpoint): 76 | """ Utility to load an RST file and turn it into fancy HTML. """ 77 | 78 | rst = endpoint.__doc__ 79 | if isinstance(rst, six.binary_type): 80 | rst = rst.decode('utf-8') 81 | 82 | rst = modify_rst(rst) 83 | 84 | api_docs = docutils.examples.html_body(rst) 85 | 86 | api_docs = modify_html(api_docs) 87 | 88 | api_docs = markupsafe.Markup(api_docs) 89 | return api_docs 90 | -------------------------------------------------------------------------------- /fedocal/fedocallib/dbaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | dbaction - Simple file containing all the methods to add/edit and remove 5 | object from the database. 6 | 7 | Copyright (C) 2012-2014 Pierre-Yves Chibon 8 | Author: Pierre-Yves Chibon 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or (at 13 | your option) any later version. 14 | See http://www.gnu.org/copyleft/gpl.html for the full text of the 15 | license. 16 | """ 17 | from __future__ import unicode_literals, absolute_import, print_function 18 | 19 | from datetime import date 20 | from fedocal.fedocallib import model 21 | 22 | 23 | def add_reminder( 24 | session, remind_when, reminder_from, 25 | remind_who, reminder_text=None): 26 | """ Logic to add a reminder to the database. 27 | """ 28 | reminder = model.Reminder( 29 | reminder_offset=remind_when, 30 | reminder_from=reminder_from, 31 | reminder_to=remind_who, 32 | reminder_text=reminder_text) 33 | reminder.save(session) 34 | session.flush() 35 | return reminder 36 | 37 | 38 | # pylint: disable=R0913,R0914 39 | def add_meeting( 40 | session, 41 | meeting_name, 42 | meeting_manager, 43 | meeting_date, 44 | meeting_date_end, 45 | meeting_time_start, 46 | meeting_time_stop, 47 | meeting_information, 48 | calendarobj, 49 | meeting_timezone='UTC', 50 | reminder_id=None, 51 | meeting_location=None, 52 | recursion_frequency=None, 53 | recursion_ends=None, 54 | full_day=False): 55 | """ Logic to add a meeting to the database. 56 | """ 57 | 58 | if not recursion_frequency: 59 | recursion_frequency = None 60 | if not recursion_ends and recursion_frequency: 61 | recursion_ends = date(2025, 12, 31) 62 | 63 | # Unless we test directly dbaction we will never hit this as 64 | # fedocallib.add_meeting already covers it 65 | if not meeting_date_end: # pragma: no cover 66 | meeting_date_end = meeting_date 67 | 68 | meeting = model.Meeting( 69 | meeting_name=meeting_name, 70 | meeting_date=meeting_date, 71 | meeting_date_end=meeting_date_end, 72 | meeting_time_start=meeting_time_start.time(), 73 | meeting_time_stop=meeting_time_stop.time(), 74 | meeting_timezone=meeting_timezone, 75 | meeting_information=meeting_information, 76 | calendar_name=calendarobj.calendar_name, 77 | reminder_id=reminder_id, 78 | meeting_location=meeting_location, 79 | recursion_frequency=recursion_frequency, 80 | recursion_ends=recursion_ends, 81 | full_day=full_day) 82 | meeting.save(session) 83 | meeting.add_manager(session, meeting_manager) 84 | meeting.save(session) 85 | session.commit() 86 | return meeting 87 | -------------------------------------------------------------------------------- /tests/test_reminder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal.model test script 28 | """ 29 | from __future__ import unicode_literals, absolute_import, print_function 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | from sqlalchemy.exc import IntegrityError, DataError 36 | 37 | sys.path.insert(0, os.path.join(os.path.dirname( 38 | os.path.abspath(__file__)), '..')) 39 | 40 | from fedocal.fedocallib import model 41 | from tests import Modeltests 42 | 43 | 44 | # pylint: disable=C0103 45 | class Remindertests(Modeltests): 46 | """ Reminder tests. """ 47 | 48 | def test_init_reminder(self): 49 | """ Test the Reminder init function. """ 50 | obj = model.Reminder( 51 | 'H-12', 52 | 'pingou@fp.o', 53 | 'fi-apprentice@lists.fedoraproject.org,' 54 | 'ambassadors@lists.fedoraproject.org', 55 | 'This is your friendly reminder') 56 | obj.save(self.session) 57 | self.session.commit() 58 | self.assertNotEqual(obj, None) 59 | 60 | def test_repr_reminder(self): 61 | """ Test the Reminder string representation function. """ 62 | self.test_init_reminder() 63 | obj = model.Reminder.by_id(self.session, 1) 64 | self.assertNotEqual(obj, None) 65 | self.assertEqual( 66 | str(obj), 67 | '') 69 | 70 | def test_get_reminder(self): 71 | """ Test the query of a reminder by its identifier. """ 72 | self.test_init_reminder() 73 | obj = model.Reminder.by_id(self.session, 1) 74 | self.assertNotEqual(obj, None) 75 | self.assertEqual(obj.reminder_offset, 'H-12') 76 | self.assertEqual( 77 | obj.reminder_to, 78 | 'fi-apprentice@lists.fedoraproject.org,' 79 | 'ambassadors@lists.fedoraproject.org') 80 | self.assertEqual( 81 | obj.reminder_text, 'This is your friendly reminder') 82 | 83 | if __name__ == '__main__': 84 | SUITE = unittest.TestLoader().loadTestsFromTestCase(Remindertests) 85 | unittest.TextTestRunner(verbosity=2).run(SUITE) 86 | -------------------------------------------------------------------------------- /doc/deployment.rst: -------------------------------------------------------------------------------- 1 | Deployment 2 | ========== 3 | 4 | From sources 5 | ------------ 6 | 7 | Clone the source:: 8 | 9 | git clone https://pagure.io/fedocal.git 10 | 11 | 12 | Copy the configuration files:: 13 | 14 | cp fedocal.cfg.sample fedocal.cfg 15 | cp alembic.ini.sample alembic.ini 16 | 17 | Adjust the configuration files (secret key, database URL, admin group...). 18 | See :doc:`configuration` for detailed information about the configuration. 19 | 20 | 21 | Create the database scheme:: 22 | 23 | sh createdb 24 | 25 | or:: 26 | 27 | FEDOCAL_CONFIG=/path/to/fedocal.cfg python createdb.py 28 | 29 | Set up the WSGI as described below. 30 | 31 | If you want to get your instance translated, do not forget to compile translations:: 32 | 33 | pybabel compile -d fedocal/translations 34 | 35 | Fedocal translations uses gettext with the Flask-Babel extension. If this 36 | extension is not available on your systme, it will silently fall back to 37 | the main (english) language. 38 | 39 | 40 | From system-wide packages 41 | ------------------------- 42 | 43 | Start by install fedocal:: 44 | 45 | yum install fedocal 46 | 47 | Adjust the configuration files: ``/etc/fedocal/fedocaf.cfg`` and 48 | ``/etc/fedocal/alembic.ini``. 49 | See :doc:`configuration` for detailed information about the configuration. 50 | 51 | Find the file used to create the database:: 52 | 53 | rpm -ql fedocal |grep createdb.py 54 | 55 | Create the database scheme:: 56 | 57 | FEDOCAL_CONFIG=/etc/fedocal/fedocal.cfg python path/to/createdb.py 58 | 59 | Set up the WSGI as described below. 60 | 61 | 62 | Set-up WSGI 63 | ----------- 64 | 65 | Start by installing ``mod_wsgi``:: 66 | 67 | yum install mod_wsgi 68 | 69 | 70 | Then configure apache:: 71 | 72 | sudo vim /etc/httd/conf.d/fedocal.conf 73 | 74 | uncomment the content of the file and adjust as desired. 75 | 76 | 77 | Then edit the file ``/usr/share/fedocal/fedocal.wsgi`` and 78 | adjust as needed. 79 | 80 | 81 | Then restart apache and you should be able to access the website on 82 | http://localhost/fedocal 83 | 84 | 85 | .. note:: `Flask `_ provides also some documentation 86 | on how to `deploy Flask application with WSGI and apache 87 | `_. 88 | 89 | 90 | Set-up the cron job 91 | ------------------- 92 | 93 | Reminders are sent by a cron job which is provided with the source under 94 | the name ``fedocal_cron.py``. 95 | 96 | You will need to specify the ``FEDOCAL_CONFIG`` environment variable 97 | when running the cron job as the specified configuration file contains 98 | information required by the cron job (ie: SMTP_SERVER or CRON_FREQUENCY, 99 | see :doc:`configuration`). 100 | 101 | The only tricky part is that the configuration file will need to be 102 | adjusted according to how the cron job is set-up. See :doc:`configuration` 103 | on how to set-up the configuration file. 104 | 105 | Example of the cron job: 106 | 107 | :: 108 | 109 | */30 * * * * FEDOCAL_CONFIG=/etc/fedocal/fedocal.cfg /usr/bin/fedocal_cron.py 110 | 111 | 112 | 113 | For testing 114 | ----------- 115 | 116 | See :doc:`development` if you want to run fedocal just to test it. 117 | 118 | -------------------------------------------------------------------------------- /fedocal/templates/default/my_meeting.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block title %}{{ _('My meetings') }}{% endblock %} 5 | 6 | {%block tag %}mymeeting{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{{ _('Manage your meetings') }}

12 |

{{ _('Below is the list of your past and future meetings.') }}

13 |
14 | 15 |

{{ _('Meetings times are expressed in the %(timezone)s time zone.', timezone=tzone) }}

16 | 17 |

{{ _('Future meetings') }}

18 | {% if single_meetings %} 19 |

{{ _('This is the list of all non-recurrent meeting you created') }}

20 |
21 | 22 | {% for meeting in single_meetings %} 23 | 24 | 25 | 29 | 30 | 31 | 43 | 44 | {% endfor %} 45 |
{{ meeting.meeting_date }} 26 | {{ meeting.meeting_time_start }} to 27 | {{ meeting.meeting_time_stop }} 28 | {{ meeting.calendar_name }} {{ meeting.meeting_name }} 32 | 34 | {{ _('Edit') }} 36 | 37 | 39 | {{ _('Delete') }} 41 | 42 |
46 |
47 | {% endif %} 48 | 49 | {% if regular_meetings %} 50 |

{{ _('This is the list of all regular meeting you created') }}

51 |
52 | 53 | {% for meeting in regular_meetings %} 54 | 55 | 56 | 59 | 60 | 61 | 73 | 74 | {% endfor %} 75 |
{{ meeting.meeting_date }} 57 | {{ _('%(start)s to %(end)s', start=meeting.meeting_time_start, end=meeting.meeting_time_stop) }} 58 | {{ meeting.calendar_name }} {{ meeting.meeting_name }} 62 | 64 | {{ _('Edit') }} 66 | 67 | 69 | {{ _('Delete') }} 71 | 72 |
76 |
77 | {% endif %} 78 | 79 |

{{ _('Past meetings') }}

80 |
81 | 82 | {% for meeting in pas_meetings %} 83 | 84 | 85 | 88 | 89 | 90 | 91 | {% endfor %} 92 |
{{ meeting.meeting_date }} 86 | {{ _('%(start)s to %(end)s', start=meeting.meeting_time_start, end=meeting.meeting_time_stop) }} 87 | {{ meeting.calendar_name }} {{ meeting.meeting_name }}
93 |
94 |
95 | {% endblock %} 96 | -------------------------------------------------------------------------------- /doc/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | There are the main configuration options to set to have fedocal running. 5 | These options are all present and described in the fedocal.cfg file. 6 | 7 | The secret key 8 | --------------- 9 | 10 | Set in the configuration file under the key ``SECRET_KEY``, this is a unique, 11 | random string which is used by `Flask `_ to generate 12 | the `CSRF `_ key unique for each user. 13 | 14 | 15 | You can easily generate one using `pwgen `_ 16 | for example to generate a 50 characters long random key 17 | :: 18 | 19 | pwgen 50 20 | 21 | 22 | The database URL 23 | ----------------- 24 | 25 | Fedocal uses `SQLAlchemy `_ has Object Relationship 26 | Mapper and thus to connect to the database. You need to provide under the 27 | key ``DB_URL`` in the configuration file the required information to connect 28 | to the database. 29 | 30 | 31 | Examples URLs are:: 32 | 33 | DB_URL=mysql://user:pass@host/db_name 34 | DB_URL=postgres://user:pass@host/db_name 35 | DB_URL=sqlite:////full/path/to/database.sqlite 36 | 37 | 38 | .. note:: The key ``sqlalchemy.url`` of the ``alembic.ini`` file should 39 | have the same value as the ``DB_URL`` described here. 40 | 41 | 42 | The admin group 43 | ---------------- 44 | 45 | Fedocal relies on a group of administrator to create calendar which are then 46 | managed by people from this group. The ``ADMIN_GROUP`` field in the 47 | configuration file refers to the 48 | `FAS `_ group that manages this 49 | fedocal instance. 50 | 51 | See :doc:`usage` for details explanations on the different administration layer 52 | of fedocal. 53 | 54 | 55 | The SMTP server address 56 | ----------------------- 57 | 58 | Fedocal sends reminder emails for the meeting for which it has been asked. 59 | This tasks is performed by a cron job. 60 | The ``SMTP_SERVER`` field in the configuration file refers to the address 61 | of the `SMTP `_ 62 | server to use to send these reminders. 63 | 64 | 65 | This field defaults to ``SMTP_SERVER='localhost'``. 66 | 67 | 68 | The cron job frequency 69 | ---------------------- 70 | 71 | Fedocal sends reminder emails for the meeting for which it has been asked. 72 | This tasks is performed by a cron job. 73 | The ``CRON_FREQUENCY`` field in the configuration file refers to the 74 | time (in minute) spent between two consecutive run of the cron job. This 75 | information is essentiel to accurately retrieve the meetings to remind 76 | and avoid sending multiple reminder for one meeting. 77 | 78 | 79 | This field defaults to ``CRON_FREQUENCY=30``. 80 | 81 | 82 | Dedicated theme 83 | --------------- 84 | 85 | Fedocal supports having multiple theme. To make your own theme, copy the 86 | theme `default` and adjust it as desired. 87 | 88 | Remember: 89 | * All the templates from the default theme should exists in your own theme 90 | * Copy the templates and the static files 91 | 92 | You can then change from one theme to another simply by using the configuration 93 | key ``THEME_FOLDER`` in the configuration file. 94 | 95 | .. note:: the template and static file of the theme should be located under 96 | the default ``template`` and ``static`` folder, where are currently 97 | located to folder named ``default`` representing the default theme. 98 | 99 | 100 | Upload check 101 | ------------ 102 | 103 | From the version 0.4.0, one may upload an iCalendar file into a specified 104 | calendar. However fedocal runs some check on the file uploaded, including its 105 | file extension and its mimetype. 106 | 107 | The configuration file allows to specify a list of allowed file extension, the 108 | `ALLOWED_EXTENSIONS` key and mimetype, the `ALLOWED_MIMETYPES` key. 109 | 110 | Adjust as desired. 111 | 112 | 113 | Exception email 114 | --------------- 115 | 116 | From the version 0.4.0, fedocal sends email on un-catched exception causing 500 117 | errors to the user. The configuration key `EMAIL_ERROR` allows to set to who the 118 | exception emails will be sent. 119 | -------------------------------------------------------------------------------- /fedocal/static/default/jquery.countdown.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * The Final Countdown for jQuery v2.0.3 (http://hilios.github.io/jQuery.countdown/) 3 | * Copyright (c) 2014 Edson Hilios 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | * this software and associated documentation files (the "Software"), to deal in 7 | * the Software without restriction, including without limitation the rights to 8 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | * the Software, and to permit persons to whom the Software is furnished to do so, 10 | * 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, FITNESS 17 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | */ 22 | !function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){"use strict";function b(a){if(a instanceof Date)return a;if(String(a).match(g))return String(a).match(/^[0-9]*$/)&&(a=Number(a)),String(a).match(/\-/)&&(a=String(a).replace(/\-/g,"/")),new Date(a);throw new Error("Couldn't cast `"+a+"` to a date object.")}function c(a){return function(b){var c=b.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi);if(c)for(var e=0,f=c.length;f>e;++e){var g=c[e].match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/),i=new RegExp(g[0]),j=g[1]||"",k=g[3]||"",l=null;g=g[2],h.hasOwnProperty(g)&&(l=h[g],l=Number(a[l])),null!==l&&("!"===j&&(l=d(k,l)),""===j&&10>l&&(l="0"+l.toString()),b=b.replace(i,l.toString()))}return b=b.replace(/%%/,"%")}}function d(a,b){var c="s",d="";return a&&(a=a.replace(/(:|;|\s)/gi,"").split(/\,/),1===a.length?c=a[0]:(d=a[0],c=a[1])),1===Math.abs(b)?d:c}var e=100,f=[],g=[];g.push(/^[0-9]*$/.source),g.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/.source),g.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/.source),g=new RegExp(g.join("|"));var h={Y:"years",m:"months",w:"weeks",d:"days",D:"totalDays",H:"hours",M:"minutes",S:"seconds"},i=function(b,c,d){this.el=b,this.$el=a(b),this.interval=null,this.offset={},this.instanceNumber=f.length,f.push(this),this.$el.data("countdown-instance",this.instanceNumber),d&&(this.$el.on("update.countdown",d),this.$el.on("stoped.countdown",d),this.$el.on("finish.countdown",d)),this.setFinalDate(c),this.start()};a.extend(i.prototype,{start:function(){null!==this.interval&&clearInterval(this.interval);var a=this;this.update(),this.interval=setInterval(function(){a.update.call(a)},e)},stop:function(){clearInterval(this.interval),this.interval=null,this.dispatchEvent("stoped")},pause:function(){this.stop.call(this)},resume:function(){this.start.call(this)},remove:function(){this.stop(),f[this.instanceNumber]=null,delete this.$el.data().countdownInstance},setFinalDate:function(a){this.finalDate=b(a)},update:function(){return 0===this.$el.closest("html").length?void this.remove():(this.totalSecsLeft=this.finalDate.getTime()-(new Date).getTime(),this.totalSecsLeft=Math.ceil(this.totalSecsLeft/1e3),this.totalSecsLeft=this.totalSecsLeft<0?0:this.totalSecsLeft,this.offset={seconds:this.totalSecsLeft%60,minutes:Math.floor(this.totalSecsLeft/60)%60,hours:Math.floor(this.totalSecsLeft/60/60)%24,days:Math.floor(this.totalSecsLeft/60/60/24)%7,totalDays:Math.floor(this.totalSecsLeft/60/60/24),weeks:Math.floor(this.totalSecsLeft/60/60/24/7),months:Math.floor(this.totalSecsLeft/60/60/24/30),years:Math.floor(this.totalSecsLeft/60/60/24/365)},void(0===this.totalSecsLeft?(this.stop(),this.dispatchEvent("finish")):this.dispatchEvent("update")))},dispatchEvent:function(b){var d=a.Event(b+".countdown");d.finalDate=this.finalDate,d.offset=a.extend({},this.offset),d.strftime=c(this.offset),this.$el.trigger(d)}}),a.fn.countdown=function(){var b=Array.prototype.slice.call(arguments,0);return this.each(function(){var c=a(this).data("countdown-instance");if(void 0!==c){var d=f[c],e=b[0];i.prototype.hasOwnProperty(e)?d[e].apply(d,b.slice(1)):null===String(e).match(/^[$A-Z_][0-9A-Z_$]*$/i)?(d.setFinalDate.call(d,e),d.start()):a.error("Method %s does not exist on jQuery.countdown".replace(/\%s/gi,e))}else new i(this,b[0],b[1])})}}); -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | fedocal 2 | ======= 3 | 4 | :Author: Pierre-Yves Chibon 5 | 6 | 7 | fedocal is a web based calendar application. 8 | 9 | 10 | Get this project: 11 | ----------------- 12 | Source: https://pagure.io/fedocal.git/ 13 | Mirror on github: https://github.com/fedora-infra/fedocal 14 | (Please use Pagure as the main repository and make sure 15 | you run your patch against it) 16 | 17 | Documentation: http://fedocal.rtfd.org 18 | 19 | 20 | Dependencies: 21 | ------------- 22 | .. _Flask: http://flask.pocoo.org/ 23 | .. _SQLAlchemy: http://www.sqlalchemy.org/ 24 | .. _alembic: https://bitbucket.org/zzzeek/alembic 25 | .. _iCal: http://en.wikipedia.org/wiki/ICalendar 26 | .. _python-vobject: http://vobject.skyhouseconsulting.com/ 27 | .. _pytz: http://pytz.sourceforge.net/ 28 | .. _dateutil: http://labix.org/python-dateutil 29 | 30 | This project is a `Flask`_ application. The calendars and meetings are 31 | stored into a relational database using `SQLAlchemy`_ as Object Relational 32 | Mapper (ORM) and `alembic`_ to handle database scheme changes. 33 | fedocal provides an `iCal`_ feed for each calendar and relies on 34 | `python-vobject`_ for this. Finally, `pytz`_ is used to handle the timezone 35 | changes and `dateutil`_ to allow date manipulation over months/years. 36 | 37 | The full list of dependencies can be found ine the ``requirements.txt`` file. 38 | 39 | 40 | Running a development instance: 41 | ------------------------------- 42 | 43 | Clone the source:: 44 | 45 | git clone https://github.com/fedora-infra/fedocal.git 46 | 47 | 48 | Copy the configuration files:: 49 | 50 | cp fedocal.cfg.sample fedocal.cfg 51 | cp alembic.ini.sample alembic.ini 52 | 53 | Adjust the configuration file (secret key, database URL, admin group...) 54 | 55 | 56 | Create the database scheme:: 57 | 58 | FEDOCAL_CONFIG=fedocal.cfg sh createdb 59 | 60 | 61 | Register the application to iddev for development:: 62 | 63 | oidc-register https://iddev.fedorainfracloud.org/ http://localhost:5000/oidc_callback 64 | 65 | 66 | Add the following line in your configuration file `fedocal.cfg`:: 67 | 68 | OIDC_ID_TOKEN_COOKIE_SECURE = False 69 | 70 | 71 | Run the server:: 72 | 73 | python runserver.py --config fedocal.cfg 74 | 75 | You should be able to access the server at http://localhost:5000 (do not use 76 | ``127.0.0.1`` as it will no work) 77 | 78 | 79 | /!\ If login in does not work and gives you an ``invalid return_uri`` check 80 | the ``redirect_uris`` in the ``client_secrets.json`` file and make sure it 81 | matches **exactly** (check http vs https, trailing slash vs no trailing slash...). 82 | You may have to re-register as editing directly the ``client_secrets.json`` 83 | file will not work. 84 | 85 | 86 | Git Branches: 87 | ------------- 88 | 89 | This project has three principal git branches: 90 | 91 | * ``master``: this is the default branch in which development is done. It is the 92 | branch from which releases are made. It is the branch feature branches are 93 | branched from and get merged into. 94 | * ``staging``: fedocal is deployed in the Fedora infrastructure in openshift 95 | using s2i, the staging environment is built from this branch. This branch has 96 | one extra commit that is not in ``master`` which adds all the files and changes 97 | necessary to deploy fedocal in openshift using s2i. This branch is therefore 98 | rebased and force-pushed when the staging fedocal instance is updated. 99 | Pull-Request should not target this branch. 100 | * ``production``: this branch is used for the same situation as the ``staging`` 101 | branch but for the production fedocal instance. It also contains an additional 102 | commit that is not in the ``master`` branch and is therefore force-pushed 103 | after a rebase when the production fedocal instance is updated. 104 | Pull-Request should not target this branch. 105 | 106 | 107 | Testing: 108 | -------- 109 | 110 | This project contains unit-tests allowing you to check if your server 111 | has all the dependencies correctly set. 112 | 113 | To run them simply call:: 114 | 115 | tox 116 | 117 | .. note:: To stop the test at the first error or failure you can try: 118 | 119 | :: 120 | 121 | tox -- -x 122 | 123 | .. note:: To run a single file you can try: 124 | 125 | :: 126 | 127 | tox -- tests/test_flask.py -x 128 | 129 | 130 | Reporting issues: 131 | ----------------- 132 | 133 | For any issue you may encounter please file a ticket and submit it to: 134 | 135 | Fedocal Pagure: https://pagure.io/fedocal/issues 136 | 137 | Contributors can use the same tracker to find existing bugs to work on. 138 | You need to login with your FAS account to submit or modify a ticket. 139 | 140 | 141 | 142 | License: 143 | -------- 144 | 145 | This project is licensed GPLv3+. 146 | -------------------------------------------------------------------------------- /fedocal/templates/centos/master.html: -------------------------------------------------------------------------------- 1 | {% from "_formhelpers.html" import render_field, render_field_invert, 2 | render_field_in_row %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} - Centocal 8 | 10 | 12 | 14 | 17 | {% block header %}{% endblock %} 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 33 | 34 | 35 | 49 | 50 | 51 | {% if g.fas_user %} 52 | {{ _('logged in as') }} 53 | {{ g.fas_user.username }} | 54 | {{ _('log out') }} 55 | {% else %} 56 | {{ _('login') }} 57 | {% endif %} 58 | 59 | 60 |
61 | 62 | {% with messages = get_flashed_messages(with_categories=true) %} 63 | {% if category, messages %} 64 |
    65 | {% for category, message in messages %} 66 |
  • {{ message }}
  • 67 | {% endfor %} 68 |
69 | {% endif %} 70 | {% endwith %} 71 | 72 | {% block content %}{% endblock %} 73 | 74 |
75 | 76 | 85 | 86 |
87 |
88 | 89 | {% block sidebar %}{% endblock %} 90 | 91 | {% block jscripts %} 92 | 96 | 100 | 116 | {% endblock %} 117 | 118 | 119 | -------------------------------------------------------------------------------- /fedocal_cron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012 - Copyright Pierre-Yves Chibon 6 | 7 | Distributed under License GPLv3 or later 8 | You can find a copy of this license on the website 9 | http://www.gnu.org/licenses/gpl.html 10 | 11 | This program is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program; if not, write to the Free Software 23 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 24 | MA 02110-1301, USA. 25 | 26 | This script is meant to be run as a cron job to send the reminders for 27 | each meeting that asked for it. 28 | """ 29 | from __future__ import unicode_literals, absolute_import 30 | 31 | import smtplib 32 | import warnings 33 | import logging 34 | 35 | from email.mime.text import MIMEText 36 | 37 | import fedocal 38 | import fedocal.fedocallib as fedocallib 39 | import fedocal.fedocallib.fedmsgshim as fedmsg 40 | 41 | _log = logging.getLogger(__name__) 42 | 43 | def fedmsg_publish(meeting, meeting_id): 44 | """ Publish the meeting.reminder messages on fedora-messaging. 45 | :arg meeting: a Meeting object from fedocallib.model 46 | :arg meeting_id: an int representing the meeting identifier in the 47 | database 48 | """ 49 | _log.info('Publishing a message for meeting: %s', meeting) 50 | print('Publishing a message for meeting: %s' % meeting) 51 | 52 | meeting_dict = meeting.to_json() 53 | meeting_dict['meeting_id'] = meeting_id 54 | 55 | message = dict( 56 | meeting=meeting_dict, 57 | calendar=meeting.calendar.to_json() 58 | ) 59 | fedmsg.publish(topic='meeting.reminder', msg=message) 60 | 61 | 62 | def send_reminder_meeting(meeting, meeting_id): 63 | """ This function sends the actual reminder of a given meeting. 64 | :arg meeting: a Meeting object from fedocallib.model 65 | :arg meeting_id: an int representing the meeting identifier in the 66 | database 67 | """ 68 | if not meeting.reminder_id: 69 | return 70 | 71 | _log.info("Sending email reminder about meeting: %s", meeting) 72 | print("Sending email reminder about meeting: %s" % meeting) 73 | 74 | location = '' 75 | if meeting.meeting_location: 76 | location = 'At %s' % meeting.meeting_location 77 | 78 | string = u"""Dear all, 79 | 80 | You are kindly invited to the meeting: 81 | %(name)s on %(date)s from %(time_start)s to %(time_stop)s %(timezone)s 82 | %(location)s 83 | 84 | The meeting will be about: 85 | %(description)s 86 | 87 | 88 | Source: %(host)s/meeting/%(id)s/ 89 | 90 | """ % ({ 91 | 'name': u'%s' % meeting.meeting_name, 92 | 'date': meeting.meeting_date, 93 | 'time_start': meeting.meeting_time_start, 94 | 'time_stop': meeting.meeting_time_stop, 95 | 'timezone': meeting.meeting_timezone, 96 | 'location': u'%s' % location, 97 | 'description': u'%s' % meeting.meeting_information, 98 | 'id': meeting_id, 99 | 'host': fedocal.APP.config['SITE_URL'], 100 | }) 101 | 102 | if meeting.reminder.reminder_text: 103 | string = string + u""" 104 | Please note: 105 | %s""" % meeting.reminder.reminder_text 106 | msg = MIMEText(string.encode('utf-8'), 'plain', 'utf-8') 107 | msg['Subject'] = '[Fedocal] Reminder meeting : %s' % meeting.meeting_name 108 | from_email = meeting.meeting_manager[0] 109 | from_email = '%s@fedoraproject.org' % from_email 110 | msg['From'] = meeting.reminder.reminder_from or from_email 111 | msg['To'] = meeting.reminder.reminder_to.replace(',', ', ') 112 | 113 | # Send the message via our own SMTP server, but don't include the 114 | # envelope header. 115 | smtp = smtplib.SMTP(fedocal.APP.config['SMTP_SERVER']) 116 | smtp.sendmail( 117 | from_email, 118 | meeting.reminder.reminder_to.split(','), 119 | msg.as_string()) 120 | smtp.quit() 121 | return msg 122 | 123 | 124 | def send_reminder(): 125 | """ Retrieve all the meeting for which we should send a reminder and 126 | do it. 127 | """ 128 | db_url = fedocal.APP.config['DB_URL'] 129 | session = fedocallib.create_session(db_url) 130 | meetings = fedocallib.retrieve_meeting_to_remind( 131 | session, offset=int(fedocal.APP.config['CRON_FREQUENCY'])) 132 | 133 | msgs = [] 134 | for meeting in meetings: 135 | meeting_id = meeting.meeting_id 136 | meeting = fedocallib.update_date_rec_meeting(meeting, action='next') 137 | _log.info("Processing meeting: %s", meeting) 138 | print("Processing meeting: %s" % meeting) 139 | msgs.append(send_reminder_meeting(meeting, meeting_id)) 140 | fedmsg_publish(meeting, meeting_id) 141 | 142 | return msgs 143 | 144 | 145 | if __name__ == '__main__': 146 | send_reminder() 147 | -------------------------------------------------------------------------------- /fedocal/templates/default/master.html: -------------------------------------------------------------------------------- 1 | {% from "_formhelpers.html" import render_field, render_field_invert, 2 | render_field_in_row %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} - Fedocal 8 | 10 | 12 | 14 | 17 | {% block header %}{% endblock %} 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 33 | 34 | 35 | 49 | 50 | 51 | {% if g.fas_user %} 52 | {{ _('logged in as') }} 53 | {{ g.fas_user.username }} | 54 | {{ _('log out') }} 55 | {% else %} 56 | {{ _('login') }} 57 | {% endif %} 58 | 59 | 60 |
61 | 62 | {% with messages = get_flashed_messages(with_categories=true) %} 63 | {% if category, messages %} 64 |
    65 | {% for category, message in messages %} 66 |
  • {{ message }}
  • 67 | {% endfor %} 68 |
69 | {% endif %} 70 | {% endwith %} 71 | 72 | {% block content %}{% endblock %} 73 | 74 |
75 | 76 | 85 | 86 |
87 |
88 | 89 | {% block sidebar %}{% endblock %} 90 | 91 | {% block jscripts %} 92 | 96 | 100 | 117 | 118 | {% if config['FEDMENU_URL'] %} 119 | 120 | 127 | {% endif %} 128 | 129 | {% endblock %} 130 | 131 | 132 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012-2014 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal.model test script 28 | """ 29 | from __future__ import unicode_literals, absolute_import, print_function 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | from contextlib import contextmanager 36 | from datetime import date 37 | from datetime import timedelta 38 | from functools import wraps 39 | 40 | import six 41 | from sqlalchemy import create_engine 42 | from sqlalchemy.orm import sessionmaker 43 | from sqlalchemy.orm import scoped_session 44 | 45 | sys.path.insert(0, os.path.join(os.path.dirname( 46 | os.path.abspath(__file__)), '..')) 47 | 48 | from fedocal.fedocallib import model, get_start_week 49 | 50 | 51 | DB_PATH = 'sqlite:///:memory:' 52 | FAITOUT_URL = 'http://faitout.fedorainfracloud.org/' 53 | 54 | if os.environ.get('BUILD_ID'): 55 | try: 56 | import requests 57 | req = requests.get('%s/new' % FAITOUT_URL) 58 | if req.status_code == 200: 59 | DB_PATH = req.text 60 | print('Using faitout at: %s' % DB_PATH) 61 | except: 62 | pass 63 | 64 | 65 | TODAY = get_start_week(date.today().year, date.today().month, 66 | date.today().day) + timedelta(days=2) 67 | 68 | ICS_FILE = os.path.join(os.path.dirname( 69 | os.path.abspath(__file__)), 'ical.ics') 70 | ICS_FILE_NOTOK = os.path.join(os.path.dirname( 71 | os.path.abspath(__file__)), 'ical_wrong.txt') 72 | 73 | 74 | @contextmanager 75 | def user_set(APP, user): 76 | """ Set the provided user as fas_user in the provided application.""" 77 | 78 | # Hack used to remove the before_request function set by 79 | # flask.ext.fas_openid.FAS which otherwise kills our effort to set a 80 | # flask.g.fas_user. 81 | from flask import appcontext_pushed, g 82 | APP.before_request_funcs[None] = [] 83 | 84 | def handler(sender, **kwargs): 85 | g.fas_user = user 86 | g.oidc_id_token = None 87 | 88 | with appcontext_pushed.connected_to(handler, APP): 89 | yield 90 | 91 | 92 | class Modeltests(unittest.TestCase): 93 | """ Model tests. """ 94 | 95 | def __init__(self, method_name='runTest'): 96 | """ Constructor. """ 97 | unittest.TestCase.__init__(self, method_name) 98 | self.session = None 99 | 100 | # pylint: disable=C0103 101 | def setUp(self): 102 | """ Set up the environnment, ran before every tests. """ 103 | self.session = model.create_tables(DB_PATH) 104 | 105 | # pylint: disable=C0103 106 | def tearDown(self): 107 | """ Remove the test.db database if there is one. """ 108 | self.session.close() 109 | if os.path.exists(DB_PATH): 110 | os.unlink(DB_PATH) 111 | if DB_PATH.startswith('postgres'): 112 | if 'localhost' in DB_PATH: 113 | model.drop_tables(DB_PATH, self.session.bind) 114 | else: 115 | db_name = DB_PATH.rsplit('/', 1)[1] 116 | requests.get('%s/clean/%s' % (FAITOUT_URL, db_name)) 117 | 118 | 119 | class FakeGroup(object): 120 | """ Fake object used to make the FakeUser object closer to the 121 | expectations. 122 | """ 123 | 124 | def __init__(self, name): 125 | """ Constructor. 126 | :arg name: the name given to the name attribute of this object. 127 | """ 128 | self.name = name 129 | self.group_type = 'cla' 130 | 131 | 132 | # pylint: disable=R0903 133 | class FakeUser(object): 134 | """ Fake user used to test the fedocallib library. """ 135 | 136 | def __init__(self, groups=[], username='username', cla_done=True): 137 | """ Constructor. 138 | :arg groups: list of the groups in which this fake user is 139 | supposed to be. 140 | """ 141 | if isinstance(groups, six.string_types): 142 | groups = [groups] 143 | if cla_done: 144 | groups.append("signed_fpca") 145 | self.groups = groups 146 | self.username = username 147 | self.name = username 148 | self.approved_memberships = [ 149 | FakeGroup('packager'), 150 | FakeGroup('design-team')] 151 | self.dic = {} 152 | self.dic['timezone'] = 'Europe/Paris' 153 | self.cla_done = cla_done 154 | 155 | def __getitem__(self, key): 156 | return self.dic[key] 157 | 158 | 159 | if __name__ == '__main__': 160 | SUITE = unittest.TestLoader().loadTestsFromTestCase(Modeltests) 161 | unittest.TextTestRunner(verbosity=2).run(SUITE) 162 | -------------------------------------------------------------------------------- /fedocal/mail_logging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright © 2014 Red Hat, Inc. 4 | # 5 | # This copyrighted material is made available to anyone wishing to use, 6 | # modify, copy, or redistribute it subject to the terms and conditions 7 | # of the GNU General Public License v.2, or (at your option) any later 8 | # version. This program is distributed in the hope that it will be 9 | # useful, but WITHOUT ANY WARRANTY expressed or implied, including the 10 | # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR 11 | # PURPOSE. See the GNU General Public License for more details. You 12 | # should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, 14 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 15 | # 16 | # Any Red Hat trademarks that are incorporated in the source 17 | # code or documentation are not subject to the GNU General Public 18 | # License and may only be used or replicated with the express permission 19 | # of Red Hat, Inc. 20 | # 21 | from __future__ import unicode_literals, absolute_import, print_function 22 | 23 | ''' 24 | Mail handler for logging. 25 | ''' 26 | import logging 27 | import logging.handlers 28 | 29 | import inspect 30 | import os 31 | import socket 32 | import traceback 33 | 34 | psutil = None 35 | try: 36 | import psutil 37 | except (OSError, ImportError): 38 | # We run into issues when trying to import psutil from inside mod_wsgi on 39 | # rhel7. If we hit that here, then just fail quietly. 40 | # https://github.com/jmflinuxtx/kerneltest-harness/pull/17#issuecomment-48007837 41 | pass 42 | 43 | 44 | class ContextInjector(logging.Filter): 45 | """ Logging filter that adds context to log records. 46 | 47 | Filters are typically used to "filter" log records. They declare a filter 48 | method that can return True or False. Only records with 'True' will 49 | actually be logged. 50 | 51 | Here, we somewhat abuse the concept of a filter. We always return true, 52 | but we use the opportunity to hang important contextual information on the 53 | log record to later be used by the logging Formatter. We don't normally 54 | want to see all this stuff in normal log records, but we *do* want to see 55 | it when we are emailed error messages. Seeing an error, but not knowing 56 | which host it comes from, is not that useful. 57 | 58 | http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual 59 | 60 | This code has been originally written by Ralph Bean for the fedmsg 61 | project: 62 | https://github.com/fedora-infra/fedmsg/ 63 | and can be found at: 64 | https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/fedmsg/base/templates/logging.py.j2 65 | 66 | """ 67 | 68 | def filter(self, record): 69 | current_process = ContextInjector.get_current_process() 70 | current_hostname = socket.gethostname() 71 | 72 | record.host = current_hostname 73 | record.proc = current_process 74 | record.pid = '-' 75 | if not isinstance(current_process, str): 76 | record.pid = current_process.pid 77 | record.proc_name = current_process.name() 78 | record.command_line = " ".join(current_process.cmdline()) 79 | record.callstack = self.format_callstack() 80 | return True 81 | 82 | @staticmethod 83 | def format_callstack(): 84 | for i, frame in enumerate(f[0] for f in inspect.stack()): 85 | if '__name__' not in frame.f_globals: 86 | continue 87 | modname = frame.f_globals['__name__'].split('.')[0] 88 | if modname != "logging": 89 | break 90 | 91 | def _format_frame(frame): 92 | return ' File "%s", line %i in %s\n %s' % (frame) 93 | 94 | stack = traceback.extract_stack() 95 | stack = stack[:-i] 96 | return "\n".join([_format_frame(frame) for frame in stack]) 97 | 98 | @staticmethod 99 | def get_current_process(): 100 | ''' Find out the PID of the current process. ''' 101 | mypid = os.getpid() 102 | 103 | if not psutil: 104 | return "Could not import psutil for %r" % mypid 105 | 106 | for proc in psutil.process_iter(): 107 | if proc.pid == mypid: 108 | return proc 109 | 110 | # This should be impossible. 111 | raise ValueError("Could not find process %r" % mypid) 112 | 113 | 114 | MSG_FORMAT = """Process Details 115 | --------------- 116 | host: %(host)s 117 | PID: %(pid)s 118 | name: %(proc_name)s 119 | command: %(command_line)s 120 | 121 | Message type: %(levelname)s 122 | Location: %(pathname)s:%(lineno)d 123 | Module: %(module)s 124 | Function: %(funcName)s 125 | Time: %(asctime)s 126 | 127 | Message: 128 | -------- 129 | 130 | %(message)s 131 | 132 | 133 | Callstack that lead to the logging statement 134 | -------------------------------------------- 135 | %(callstack)s 136 | """ 137 | 138 | 139 | def get_mail_handler(smtp_server, mail_admin): 140 | """ Set up the handler sending emails for big exception 141 | """ 142 | mail_handler = logging.handlers.SMTPHandler( 143 | smtp_server, 144 | 'nobody@fedoraproject.org', 145 | mail_admin, 146 | 'Fedocal error') 147 | mail_handler.setFormatter(logging.Formatter(MSG_FORMAT)) 148 | mail_handler.setLevel(logging.ERROR) 149 | mail_handler.addFilter(ContextInjector()) 150 | return mail_handler 151 | -------------------------------------------------------------------------------- /fedocal/fedocallib/week.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | fedocallib - Back-end library for the fedocal project. 5 | 6 | Copyright (C) 2012-2014 Pierre-Yves Chibon 7 | Author: Pierre-Yves Chibon 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 3 of the License, or (at 12 | your option) any later version. 13 | See http://www.gnu.org/copyleft/gpl.html for the full text of the 14 | license. 15 | """ 16 | from __future__ import unicode_literals, absolute_import, print_function 17 | 18 | from datetime import timedelta 19 | import operator 20 | from fedocal.fedocallib.model import Meeting 21 | from fedocal.fedocallib.exceptions import FedocalException 22 | 23 | from fedocal.fedocal_babel import gettext 24 | 25 | 26 | # pylint: disable=R0903 27 | class Week(object): 28 | """ This class represents a week for in a specific calendar with 29 | all its meetings. 30 | """ 31 | 32 | def __init__(self, session, calendar=None, start_date=None, 33 | location=None): 34 | """ Constructor, instanciate a week object for a given calendar. 35 | 36 | One of calendar or location should be specified! 37 | 38 | :kwarg calendar, the name of the calendar to use. 39 | :kwarg start_date, the starting date of the week. 40 | :kwarg location, the name of the location of the meetings. 41 | 42 | """ 43 | if not calendar and not location: 44 | raise FedocalException( 45 | gettext('Either calendar or location should be specified')) 46 | 47 | self.session = session 48 | self.calendar = calendar 49 | self.location = location 50 | self.start_date = start_date 51 | self.stop_date = start_date + timedelta(days=6) 52 | self.meetings = [] 53 | self.full_day_meetings = [] 54 | self.get_meetings() 55 | self.get_full_day_meetings() 56 | 57 | def get_meetings(self): 58 | """ Retrieves the list of this week's meeting from the database. 59 | """ 60 | if self.calendar: 61 | self.meetings = Meeting.get_by_date( 62 | self.session, self.calendar, 63 | self.start_date, self.stop_date, full_day=False) 64 | 65 | meetings = Meeting.get_active_regular_meeting( 66 | self.session, self.calendar, 67 | self.start_date, self.stop_date, full_day=False) 68 | else: 69 | self.meetings = Meeting.get_by_date_at_location( 70 | self.session, self.location, 71 | self.start_date, self.stop_date, full_day=False) 72 | 73 | meetings = Meeting.get_active_regular_meeting_at_location( 74 | self.session, self.location, 75 | self.start_date, self.stop_date, full_day=False) 76 | 77 | for meeting in meetings: 78 | for delta in range(0, 7): 79 | day = self.start_date + timedelta(days=delta) 80 | if ((meeting.meeting_date - day).days % 81 | meeting.recursion_frequency) == 0: 82 | if meeting not in self.meetings: 83 | self.meetings.append(meeting) 84 | # Expand the regular meetings so that they appear as meeting 85 | self.meetings = Meeting.expand_regular_meetings( 86 | self.meetings, 87 | start_date=self.start_date, 88 | end_date=self.stop_date) 89 | # Sort the meetings by date, time_start and name 90 | self.meetings.sort(key=operator.attrgetter( 91 | 'meeting_date', 'meeting_time_start', 'meeting_name')) 92 | 93 | def get_full_day_meetings(self): 94 | """ Retrieve all the full day meetings for this week. """ 95 | if self.calendar: 96 | self.full_day_meetings = Meeting.get_by_date( 97 | self.session, self.calendar, self.start_date, 98 | self.stop_date, full_day=True) 99 | 100 | meetings = Meeting.get_active_regular_meeting( 101 | self.session, self.calendar, 102 | self.start_date, self.stop_date, full_day=True) 103 | else: 104 | self.full_day_meetings = Meeting.get_by_date_at_location( 105 | self.session, self.location, self.start_date, 106 | self.stop_date, full_day=True) 107 | 108 | meetings = Meeting.get_active_regular_meeting_at_location( 109 | self.session, self.location, 110 | self.start_date, self.stop_date, full_day=True) 111 | 112 | for meeting in meetings: 113 | for delta in range(0, 7): 114 | day = self.start_date + timedelta(days=delta) 115 | if ((meeting.meeting_date - day).days % 116 | meeting.recursion_frequency) == 0: 117 | if meeting not in self.full_day_meetings: 118 | self.full_day_meetings.append(meeting) 119 | # Expand the regular meetings so that they appear as meeting 120 | self.full_day_meetings = Meeting.expand_regular_meetings( 121 | self.full_day_meetings, end_date=self.stop_date, 122 | start_date=self.start_date) 123 | # Sort the meetings by date, time_start and name 124 | self.full_day_meetings.sort(key=operator.attrgetter( 125 | 'meeting_date', 'meeting_time_start', 'meeting_name')) 126 | 127 | def __repr__(self): 128 | """ Representation of the Week object when printed. 129 | """ 130 | return "" % ( 131 | self.calendar.calendar_name, 132 | self.start_date, self.stop_date) 133 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fedocal.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fedocal.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/fedocal" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fedocal" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /alembic/update03_to_04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | """ 4 | (c) 2014 - Copyright Pierre-Yves Chibon 5 | 6 | Distributed under License GPLv3 or later 7 | You can find a copy of this license on the website 8 | http://www.gnu.org/licenses/gpl.html 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | MA 02110-1301, USA. 24 | """ 25 | 26 | 27 | ''' 28 | Utility script to convert a fedocal-0.3.x database to the fedocal-0.4.0 29 | data model. 30 | 31 | Note: It will need the SQLAlchemy database url filled at the top of this 32 | file. 33 | 34 | ''' 35 | from __future__ import print_function 36 | 37 | 38 | import sys 39 | import os 40 | 41 | import sqlalchemy as sa 42 | import sqlalchemy.orm as orm 43 | import sqlalchemy.exc 44 | 45 | 46 | sys.path.insert(0, os.path.join(os.path.dirname( 47 | os.path.abspath(__file__)), '..')) 48 | 49 | import fedocal.fedocallib 50 | from fedocal.fedocallib import model 51 | 52 | 53 | DB_URL_FEDOCAL03 = '' 54 | DB_URL_FEDOCAL04 = '' 55 | 56 | 57 | class F03Calendars(object): 58 | pass 59 | 60 | 61 | class F03Meetings(object): 62 | pass 63 | 64 | 65 | class F03Reminders(object): 66 | pass 67 | 68 | 69 | def create_session(db_url, debug=False): 70 | ''' Return a SQLAlchemy session connecting to the provided database. 71 | This is assuming the db_url provided has the right information. 72 | 73 | ''' 74 | engine = sa.create_engine(db_url, echo=debug) 75 | 76 | metadata = sa.MetaData(engine) 77 | 78 | table = sa.Table('calendars', metadata, autoload=True) 79 | sa.orm.mapper(F03Calendars, table) 80 | 81 | table = sa.Table('meetings', metadata, autoload=True) 82 | sa.orm.mapper(F03Meetings, table) 83 | 84 | table = sa.Table('reminders', metadata, autoload=True) 85 | sa.orm.mapper(F03Reminders, table) 86 | 87 | scopedsession = orm.scoped_session(orm.sessionmaker(bind=engine)) 88 | return scopedsession 89 | 90 | 91 | def convert_calendars(fed03_sess, fed04_sess): 92 | ''' Convert the Calendars from fedocal-0.3.x to fedocal-0.4.0. 93 | ''' 94 | cnt = 0 95 | for calendar in fed03_sess.query(F03Calendars).all(): 96 | calendarobj = model.Calendar( 97 | calendar_name=calendar.calendar_name, 98 | calendar_contact=calendar.calendar_contact, 99 | calendar_description=calendar.calendar_description, 100 | calendar_editor_group=calendar.calendar_editor_group, 101 | calendar_admin_group=calendar.calendar_admin_group, 102 | calendar_status=calendar.calendar_status 103 | ) 104 | fed04_sess.add(calendarobj) 105 | cnt += 1 106 | fed04_sess.commit() 107 | print('%s calendars added' % cnt) 108 | 109 | 110 | def convert_reminders(fed03_sess, fed04_sess): 111 | ''' Convert the Reminders from fedocal-0.3.x to fedocal-0.4.0. 112 | ''' 113 | cnt = 0 114 | for reminder in fed03_sess.query(F03Reminders).all(): 115 | reminderobj = model.Reminder( 116 | reminder_offset=reminder.reminder_offset, 117 | reminder_to=reminder.reminder_to, 118 | reminder_text=reminder.reminder_text, 119 | ) 120 | reminderobj.reminder_id = reminder.reminder_id 121 | fed04_sess.add(reminderobj) 122 | cnt += 1 123 | fed04_sess.commit() 124 | print('%s reminders added' % cnt) 125 | 126 | 127 | def convert_meetings(fed03_sess, fed04_sess): 128 | ''' Convert the Meetings from fedocal-0.3.x to fedocal-0.4.0. 129 | ''' 130 | cnt = 0 131 | for meeting in fed03_sess.query(F03Meetings).all(): 132 | calendarobj = model.Calendar.by_id( 133 | fed04_sess, meeting.calendar_name) 134 | 135 | if meeting.reminder_id: 136 | reminderobj = model.Reminder.by_id( 137 | fed04_sess, meeting.reminder_id) 138 | 139 | # Create the meeting 140 | meetingobj = model.Meeting( 141 | meeting_name=meeting.meeting_name, 142 | meeting_date=meeting.meeting_date, 143 | meeting_date_end=meeting.meeting_date_end, 144 | meeting_time_start=meeting.meeting_time_start, 145 | meeting_time_stop=meeting.meeting_time_stop, 146 | meeting_information=meeting.meeting_information, 147 | calendar_name=meeting.calendar_name, 148 | meeting_timezone=meeting.meeting_timezone, 149 | reminder_id=meeting.reminder_id, 150 | meeting_location=meeting.meeting_location, 151 | recursion_frequency=meeting.recursion_frequency, 152 | recursion_ends=meeting.recursion_ends, 153 | full_day=meeting.full_day) 154 | fed04_sess.add(meetingobj) 155 | # Add the managers 156 | meetingobj.add_manager(fed04_sess, meeting.meeting_manager) 157 | fed04_sess.add(meetingobj) 158 | fed04_sess.flush() 159 | cnt += 1 160 | fed04_sess.commit() 161 | print('%s meetings added' % cnt) 162 | 163 | 164 | def main(db_url_fed03, db_url_fed04): 165 | ''' The methods connect to the two pkgdb database and converts the data 166 | from one database model to the other. 167 | ''' 168 | fed03_sess = create_session(db_url_fed03) 169 | fed04_sess = fedocal.fedocallib.create_session(db_url_fed04) 170 | 171 | convert_calendars(fed03_sess, fed04_sess) 172 | convert_reminders(fed03_sess, fed04_sess) 173 | convert_meetings(fed03_sess, fed04_sess) 174 | fed03_sess.close() 175 | fed04_sess.close() 176 | 177 | 178 | if __name__ == '__main__': 179 | if not DB_URL_FEDOCAL03 or not DB_URL_FEDOCAL04: 180 | print('You need to set the database(s) URL(s) at the top of this ' \ 181 | 'file') 182 | sys.exit(1) 183 | 184 | main(DB_URL_FEDOCAL03, DB_URL_FEDOCAL04) 185 | -------------------------------------------------------------------------------- /fedocal/templates/default/view_meeting.html: -------------------------------------------------------------------------------- 1 | {% if full %} 2 | {% extends "master.html" %} 3 | 4 | {% block title %}{{ _('Meeting "%(name)s"', name=org_meeting.meeting_name) }}{% endblock %} 5 | 6 | {%block tag %}home{% endblock %} 7 | 8 | {% else %} 9 | {% extends "base_partial.html" %} 10 | {% endif %} 11 | 12 | {% block content %} 13 |
14 |

{{ _('Meeting "%(name)s"', name=org_meeting.meeting_name) }}

15 |

( 16 | {{ org_meeting.calendar_name }}) 17 |

18 | 19 | 20 | {% if org_meeting.meeting_location %} 21 |

{{ _('Location:') }} {{ org_meeting.meeting_location }}

22 | {%endif%} 23 | 24 |
    25 |
  • 27 | 28 | {{ _('Start:') }} 29 | 30 | {{ next_meeting.meeting_date.strftime('%a, %B %d, %Y') }} - {{ 31 | next_meeting.meeting_time_start.strftime('%H:%M') }} {{ tzone }} 32 |
  • 33 |
  • 35 | 36 | {{ _('End:') }} 37 | 38 | {{ next_meeting.meeting_date_end.strftime('%a, %B %d, %Y') }} - {{ 39 | next_meeting.meeting_time_stop.strftime('%H:%M') }} {{ tzone }} 40 |
  • 41 |
42 | 43 |
44 | 45 |
46 | {% autoescape off%} {{ org_meeting.meeting_information | markdown or _('No description') }} {% endautoescape %} 47 |
48 | 49 | {% if org_meeting.recursion_frequency and org_meeting.recursion_ends %} 50 |

{{ _('This meeting is recurrent, it occurs every %(frequency)s days until %(ends)s', 51 | frequency=org_meeting.recursion_frequency, ends=org_meeting.recursion_ends) }} 52 |

53 | {% endif %} 54 | 55 | {% macro get_managers(managers) %} 56 | {% for manager in managers %}{{ manager }} 57 | {%- if not loop.last %},{% endif %}{% endfor %}. 58 | {% endmacro %} 59 |

{{ _('Meeting organized by %(managers)s', managers=get_managers(org_meeting.meeting_manager)) }}

60 | 61 | 65 | {{ _('iCal export') }} 66 | 67 | {% if reminder_options %} 68 | 69 | 73 | 80 | 81 | {% endif %} 82 | 83 | {% if editor %} 84 |

{{ _('Stored as:') }}

85 |
    86 |
  • 88 | 89 | {{ _('Start:') }} 90 | 91 | {{ org_meeting.meeting_date }} - {{ org_meeting.meeting_time_start 92 | }} {{ org_meeting.meeting_timezone }}
  • 93 |
  • 95 | 96 | {{ _('End:') }} 97 | 98 | {{ org_meeting.meeting_date_end }} - {{ org_meeting.meeting_time_stop 99 | }} {{ org_meeting.meeting_timezone }}
  • 100 |
101 |

102 | 104 | {{ _('Edit') }} 106 | 107 | 109 | {{ _('Delete') }} 111 | 112 |

113 | {% endif %} 114 |
115 | {% endblock %} 116 | 117 | 118 | {% block jscripts %} 119 | {{ super() }} 120 | 123 | 148 | 151 | {% endblock %} 152 | 153 | -------------------------------------------------------------------------------- /tests/test_week.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal.model test script 28 | """ 29 | from __future__ import unicode_literals, absolute_import, print_function 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | from datetime import timedelta 36 | 37 | sys.path.insert(0, os.path.join(os.path.dirname( 38 | os.path.abspath(__file__)), '..')) 39 | 40 | from fedocal.fedocallib import model 41 | from fedocal.fedocallib import week 42 | from fedocal.fedocallib import exceptions 43 | 44 | from tests import Modeltests 45 | from .test_meeting import Meetingtests, TODAY 46 | 47 | 48 | # pylint: disable=C0103 49 | class Weektests(Modeltests): 50 | """ Week tests. """ 51 | 52 | def setUp(self): 53 | """ Set up the environnment, ran before every tests. """ 54 | super(Weektests, self).setUp() 55 | # Fills some data in the database in memory 56 | modeltest = Meetingtests(method_name='test_init_meeting') 57 | modeltest.session = self.session 58 | modeltest.test_init_meeting() 59 | 60 | def test_init_week(self): 61 | """ Test the Week init function. """ 62 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 63 | end_date = TODAY + timedelta(days=6) 64 | weekobj = week.Week(self.session, calendar, TODAY) 65 | 66 | self.assertNotEqual(weekobj, None) 67 | self.assertEqual(weekobj.start_date, TODAY) 68 | self.assertEqual(weekobj.stop_date, end_date) 69 | self.assertEqual(len(weekobj.meetings), 3) 70 | self.assertEqual(len(weekobj.full_day_meetings), 1) 71 | 72 | def test_repr_week(self): 73 | """ Test if the week string representation is correct. """ 74 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 75 | end_date = TODAY + timedelta(days=6) 76 | weekobj = week.Week(self.session, calendar, TODAY) 77 | 78 | self.assertNotEqual(weekobj, None) 79 | self.assertEqual(str(weekobj), "" % ( 80 | calendar.calendar_name, TODAY, end_date)) 81 | 82 | def test_meeting_in_week(self): 83 | """ Test that the meetings in the week are correct function. """ 84 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 85 | weekobj = week.Week(self.session, calendar, TODAY) 86 | 87 | # Test the meeting in the week 88 | self.assertNotEqual(weekobj, None) 89 | self.assertNotEqual(weekobj.meetings[0], None) 90 | self.assertEqual(len(weekobj.meetings), 3) 91 | self.assertEqual(len(weekobj.full_day_meetings), 1) 92 | 93 | self.assertEqual( 94 | weekobj.meetings[0].calendar.calendar_name, 95 | 'test_calendar') 96 | self.assertEqual( 97 | weekobj.meetings[0].calendar.calendar_description, 98 | 'This is a test calendar') 99 | 100 | self.assertEqual( 101 | weekobj.meetings[0].meeting_name, 102 | 'Another past test meeting') 103 | self.assertEqual( 104 | weekobj.meetings[0].meeting_manager, 105 | ['pingou']) 106 | self.assertEqual( 107 | weekobj.meetings[0].meeting_information, 108 | 'This is a past meeting with recursion') 109 | 110 | self.assertEqual( 111 | weekobj.meetings[1].meeting_name, 112 | 'Another test meeting2') 113 | self.assertEqual( 114 | weekobj.meetings[1].meeting_manager, 115 | ['pingou']) 116 | self.assertEqual( 117 | weekobj.meetings[1].meeting_information, 118 | 'This is a test meeting with recursion2') 119 | 120 | self.assertEqual( 121 | weekobj.meetings[2].meeting_name, 122 | 'Fedora-fr-test-meeting') 123 | self.assertEqual( 124 | weekobj.meetings[2].meeting_manager, 125 | ['pingou', 'shaiton']) 126 | self.assertEqual( 127 | weekobj.meetings[2].meeting_information, 128 | 'This is a test meeting') 129 | self.assertEqual( 130 | weekobj.meetings[2].reminder, 131 | None) 132 | 133 | self.assertEqual( 134 | weekobj.full_day_meetings[0].meeting_name, 135 | 'Full-day meeting') 136 | self.assertEqual( 137 | weekobj.full_day_meetings[0].meeting_manager, 138 | ['pingou']) 139 | self.assertEqual( 140 | weekobj.full_day_meetings[0].meeting_information, 141 | 'This is a full day meeting') 142 | 143 | def test_meeting_in_week_full_day(self): 144 | """ Test that the meetings in the week are correct function. """ 145 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 146 | weekobj = week.Week(self.session, calendar, TODAY) 147 | 148 | self.assertNotEqual(weekobj, None) 149 | self.assertEqual(len(weekobj.full_day_meetings), 1) 150 | self.assertNotEqual(weekobj.full_day_meetings[0], None) 151 | self.assertEqual( 152 | weekobj.full_day_meetings[0].meeting_name, 153 | 'Full-day meeting') 154 | 155 | weekobj = week.Week( 156 | self.session, calendar, (TODAY + timedelta(days=15))) 157 | 158 | self.assertNotEqual(weekobj, None) 159 | self.assertEqual(len(weekobj.full_day_meetings), 1) 160 | self.assertNotEqual(weekobj.full_day_meetings[0], None) 161 | self.assertEqual( 162 | weekobj.full_day_meetings[0].meeting_name, 163 | 'Full-day meeting with recursion') 164 | 165 | def test_week_exception(self): 166 | """ Test that week raises an exception if the arguments provided 167 | are incomplete. 168 | """ 169 | self.assertRaises( 170 | exceptions.FedocalException, 171 | week.Week, 172 | self.session, 173 | None, 174 | (TODAY + timedelta(days=15)) 175 | ) 176 | 177 | if __name__ == '__main__': 178 | SUITE = unittest.TestLoader().loadTestsFromTestCase(Weektests) 179 | unittest.TextTestRunner(verbosity=2).run(SUITE) 180 | -------------------------------------------------------------------------------- /tests/test_fedora_calendar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal.model test script 28 | """ 29 | from __future__ import unicode_literals, absolute_import, print_function 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | from datetime import date 36 | 37 | sys.path.insert(0, os.path.join(os.path.dirname( 38 | os.path.abspath(__file__)), '..')) 39 | 40 | from fedocal.fedocallib.fedora_calendar import FedocalCalendar 41 | from tests import Modeltests, TODAY 42 | 43 | 44 | class FedocalCalendartests(Modeltests): 45 | """ fedora_calendar application tests. """ 46 | 47 | def test_formatday(self): 48 | """ Test the formatday function. """ 49 | today = date.today() 50 | cal = FedocalCalendar(2013, 1, 6) 51 | self.assertEqual( 52 | cal.formatday(6, 1), 53 | '6') 54 | self.assertEqual( 55 | cal.formatday(7, 3), 56 | '7') 57 | 58 | def test_formatweek(self): 59 | """ Test the formatweek function. """ 60 | today = TODAY 61 | cal = FedocalCalendar(today.year, today.month, today.day) 62 | output = cal.formatweek([(1, 1), (2, 2)]).replace(' today', '') 63 | self.assertEqual( 64 | output, 65 | '12') 66 | self.assertEqual( 67 | cal.formatweek([(0, 1), (1, 2)]).replace(' today', ''), 68 | ' 1') 69 | output = cal.formatweek( 70 | [(1, 1), (2, 2)], True).replace(' today', '') 71 | self.assertEqual( 72 | output, 73 | '12') 75 | 76 | def test_formatmonthname(self): 77 | """ Test the formatmonthname function. """ 78 | today = date.today() 79 | cal = FedocalCalendar(today.year, today.month, today.day) 80 | self.assertEqual( 81 | cal.formatmonthname(2012, 4), 82 | ' April 2012 ') 83 | self.assertEqual( 84 | cal.formatmonthname(2012, 4, False), 85 | ' April ') 86 | 87 | def test_formatmonth(self): 88 | """ Test the formatmonth function. """ 89 | cal = FedocalCalendar(2012, 1, 10) 90 | 91 | expcal = [ 92 | '', 93 | '', 94 | '' 95 | '' 96 | '' 97 | '', 98 | '' 99 | '' 100 | '' 101 | '', 102 | '' 103 | '' 104 | '' 105 | '', 106 | '' 107 | '' 108 | '' 109 | '', 110 | '' 111 | '' 112 | '' 113 | '', 114 | '' 115 | '' 116 | '' 117 | '', 118 | '
January 2012
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
', ''] 119 | callines = cal.formatmonth().split('\n') 120 | 121 | self.assertEqual(len(expcal), len(callines)) 122 | for cnt in range(0, len(callines)): 123 | self.assertEqual(expcal[cnt], callines[cnt]) 124 | 125 | expcal = [ 126 | '', 127 | '', 128 | '' 129 | '' 130 | '' 131 | '', 132 | '' 133 | '' 134 | '' 135 | '', 136 | '' 137 | '' 138 | '' 139 | '', 140 | '' 141 | '' 142 | '' 143 | '', 144 | '' 145 | '' 146 | '' 147 | '', 148 | '' 149 | '' 150 | '' 151 | '', 152 | '
January
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
', ''] 153 | callines = cal.formatmonth(withyear=False).split('\n') 154 | 155 | self.assertEqual(len(expcal), len(callines)) 156 | for cnt in range(0, len(callines)): 157 | self.assertEqual(expcal[cnt], callines[cnt]) 158 | 159 | 160 | if __name__ == '__main__': 161 | SUITE = unittest.TestLoader().loadTestsFromTestCase(FedocalCalendartests) 162 | unittest.TextTestRunner(verbosity=2).run(SUITE) 163 | -------------------------------------------------------------------------------- /fedocal/fedocallib/fedora_calendar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | fedora_calendar - HTML Calendar 5 | 6 | Copyright (C) 2012-2014 Johan Cwiklinski 7 | Author: Johan Cwiklinski 8 | Pierre-Yves Chibon 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or (at 13 | your option) any later version. 14 | See http://www.gnu.org/copyleft/gpl.html for the full text of the 15 | license. 16 | """ 17 | from __future__ import unicode_literals, absolute_import, print_function 18 | 19 | import calendar 20 | import locale 21 | from datetime import date 22 | try: 23 | from calendar import TimeEncoding 24 | PY3 = False 25 | except ImportError: 26 | PY3 = True 27 | 28 | import flask 29 | 30 | import fedocal 31 | 32 | class FedocalCalendar(calendar.LocaleHTMLCalendar): 33 | """ Improve Python's HTMLCalendar object adding 34 | html validation and some features 'locally required' 35 | """ 36 | 37 | def __init__(self, year, month, day, 38 | calendar_name=None, loc_name=None, busy_days=None): 39 | """ Constructor. 40 | Stores the year and the month asked. 41 | """ 42 | try: 43 | babel_locale = fedocal.get_locale() 44 | if babel_locale: 45 | cal_locale = locale.normalize(babel_locale) 46 | if '.' in cal_locale: 47 | cal_locale = "%s.UTF-8" % cal_locale.partition('.')[0] 48 | except: 49 | pass 50 | cal_locale = 'en_EN' 51 | super(FedocalCalendar, self).__init__(locale=cal_locale) 52 | 53 | self.year = year 54 | self.month = month 55 | self.day = day 56 | self.busy_days = busy_days or [] 57 | self.calendar_name = calendar_name 58 | self.loc_name = loc_name 59 | 60 | def formatday(self, day, weekday): 61 | """ 62 | Return a day as a table cell. 63 | """ 64 | cur_date = date.today() 65 | if day == 0: 66 | return ' ' # day outside month 67 | else: 68 | link_day = day 69 | 70 | if self.calendar_name: 71 | link_day = '%d' % ( 72 | flask.url_for( 73 | 'calendar', 74 | calendar_name=self.calendar_name, year=self.year, 75 | month=self.month, day=day), 76 | day) 77 | elif self.loc_name: 78 | link_day = '%d' % ( 79 | flask.url_for( 80 | 'location', 81 | loc_name=self.loc_name, year=self.year, 82 | month=self.month, day=day), 83 | day) 84 | 85 | if day in self.busy_days: 86 | link_day = '
%s
' % link_day 87 | 88 | if day == cur_date.day \ 89 | and self.month == cur_date.month \ 90 | and self.year == cur_date.year: 91 | output = '%s' % ( 92 | self.cssclasses[weekday], link_day) 93 | else: 94 | output = '%s' % ( 95 | self.cssclasses[weekday], link_day) 96 | 97 | return output 98 | 99 | # pylint: disable=W0221 100 | def formatweek(self, theweek, current=False): 101 | """ Return a complete week as a table row. 102 | 103 | :kwarg current: a boolean stating wether this is the current 104 | week or not (the current week will have the css class: 105 | current_week) 106 | """ 107 | string = ''.join(self.formatday(d, wd) for (d, wd) in theweek) 108 | if current: 109 | return '%s' % string 110 | else: 111 | return '%s' % string 112 | 113 | def formatmonthname(self, theyear, themonth, withyear=True): 114 | """ 115 | Return a month name as a table row. 116 | """ 117 | 118 | lcle = self.locale 119 | if self.locale in ['en', 'en_EN']: 120 | lcle = 'C' 121 | 122 | if PY3: 123 | with calendar.different_locale(lcle) as encoding: 124 | smonth = calendar.month_name[themonth].capitalize() 125 | else: 126 | with TimeEncoding(lcle) as encoding: 127 | smonth = calendar.month_name[themonth].capitalize() 128 | if encoding is not None: 129 | smonth = smonth.decode(encoding).capitalize() 130 | 131 | if withyear: 132 | string = '%s %s' % (smonth, theyear) 133 | else: 134 | string = '%s' % smonth 135 | 136 | prev_month = self.month - 1 137 | prev_year = self.year 138 | if prev_month == 0: 139 | prev_month = 12 140 | prev_year = prev_year - 1 141 | 142 | prev_month_lnk = '' 143 | if self.calendar_name: 144 | prev_month_lnk = '<' % ( 145 | flask.url_for( 146 | 'calendar', 147 | calendar_name=self.calendar_name, 148 | year=int(prev_year), 149 | month=int(prev_month), 150 | day=1)) 151 | elif self.loc_name: 152 | prev_month_lnk = '<' % ( 153 | flask.url_for( 154 | 'location', 155 | loc_name=self.loc_name, 156 | year=int(prev_year), 157 | month=int(prev_month), 158 | day=1)) 159 | 160 | next_month = self.month 161 | next_year = self.year + next_month / 12 162 | next_month = next_month % 12 + 1 163 | 164 | next_month_lnk = '' 165 | if self.calendar_name: 166 | next_month_lnk = '>' % ( 167 | flask.url_for( 168 | 'calendar', 169 | calendar_name=self.calendar_name, 170 | year=int(next_year), 171 | month=int(next_month), 172 | day=1)) 173 | elif self.loc_name: 174 | next_month_lnk = '>' % ( 175 | flask.url_for( 176 | 'location', 177 | loc_name=self.loc_name, 178 | year=int(next_year), 179 | month=int(next_month), 180 | day=1)) 181 | 182 | return '%s %s %s' % ( 183 | prev_month_lnk, string, next_month_lnk) 184 | 185 | # pylint: disable=W0221 186 | def formatmonth(self, withyear=True): 187 | """ 188 | Return a formatted month as a html valid table. 189 | """ 190 | values = [] 191 | item = values.append 192 | item('') 193 | item('\n') 194 | item(self.formatmonthname(self.year, self.month, withyear=withyear)) 195 | item('\n') 196 | # item(self.formatweekheader()) 197 | # item('\n') 198 | for week in self.monthdays2calendar(self.year, self.month): 199 | days = [day[0] for day in week] 200 | if self.day in days: 201 | item(self.formatweek(week, current=True)) 202 | else: 203 | item(self.formatweek(week)) 204 | item('\n') 205 | item('
') 206 | item('\n') 207 | return ''.join(values) 208 | -------------------------------------------------------------------------------- /doc/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Get the sources 5 | --------------- 6 | 7 | Anonymous: 8 | 9 | :: 10 | 11 | git clone https://pagure.io/fedocal.git 12 | 13 | Contributors: 14 | 15 | :: 16 | 17 | git clone ssh://@git.pagure.io/fedocal.git 18 | 19 | 20 | Dependencies 21 | ------------ 22 | 23 | The dependencies of fedocal are listed in the file ``requirements.txt`` 24 | at the top level of the sources. 25 | 26 | 27 | .. note:: if you work in a `virtualenv `_ 28 | the installation of python-fedora might fail the first time you 29 | try, just try to run the command twice, the second time it should 30 | work. 31 | 32 | 33 | Run fedocal for development 34 | --------------------------- 35 | Copy the configuration file:: 36 | 37 | cp fedocal.cfg.sample fedocal.cfg 38 | 39 | Adjust the configuration file (secret key, database URL, admin group...) 40 | See :doc:`configuration` for more detailed information about the 41 | configuration. 42 | 43 | :: 44 | 45 | cp alembic.ini.sample alembic.ini 46 | 47 | Adjust the database url to be the same as in ``fedocal.cfg``. 48 | 49 | .. note:: You can use the follow entry in ``fedocal.cfg`` when working on a 50 | development version of fedocal:: 51 | 52 | import os 53 | PATH_ALEMBIC_INI=os.path.join( 54 | os.path.dirname(__file__), 'alembic.ini') 55 | 56 | Create the database scheme:: 57 | 58 | ./createdb 59 | 60 | Run the server:: 61 | 62 | ./runserver 63 | 64 | You should be able to access the server at http://localhost:5000 65 | 66 | 67 | Every time you save a file, the project will be automatically restarted 68 | so you can see your change immediatly. 69 | 70 | 71 | Coding standards 72 | ---------------- 73 | 74 | We are trying to make the code `PEP8-compliant 75 | `_. There is a `pep8 tool 76 | `_ that can automatically check 77 | your source. 78 | 79 | 80 | We are also inspecting the code using `pylint 81 | `_ and aim of course for a 10/10 code 82 | (but it is an assymptotic goal). 83 | We provide a shell script ``run_pylint.sh`` at the top of the sources to 84 | allow easy inspection of the code with pylint. 85 | 86 | .. note:: both pep8 and pylint are available in Fedora via yum: 87 | 88 | :: 89 | 90 | yum install python-pep8 pylint 91 | 92 | 93 | Send patch 94 | ---------- 95 | 96 | The easiest way to work on fedocal is to make your own branch in git, make 97 | your changes to this branch, commit whenever you want, rebase on master, 98 | whenever you need and when you are done, send the patch either by email or 99 | via the trac. 100 | 101 | 102 | The workflow would therefore be something like: 103 | 104 | :: 105 | 106 | git branch 107 | git checkout 108 | 109 | git commit file1 file2 110 | 111 | git commit file3 file4 112 | git checkout master 113 | git pull 114 | git checkout 115 | git rebase master 116 | git format-patch -2 117 | 118 | This will create two patch files that you can send by email to submit in the 119 | trac. 120 | 121 | .. note:: You can send your patch by email to the `fedocal mailing-list 122 | `_ 123 | 124 | 125 | Unit-tests 126 | ---------- 127 | 128 | Fedocal has a number of unit-tests providing at the moment a full coverage of 129 | the backend library (fedocallib). 130 | 131 | 132 | We aim at having a full (100%) coverage of the whole code (including the 133 | Flask application) and of course a smart coverage as in we want to check 134 | that the functions work the way we want but also that they fail when we 135 | expect it and the way we expect it. 136 | 137 | 138 | Tests checking that function are failing when/how we want are as important 139 | as tests checking they work the way they are intended to. 140 | 141 | ``run_tests.sh``, located at the top of the sources, helps to run the 142 | unit-tests of the project with coverage information using `python-nose 143 | `_. 144 | 145 | 146 | .. note:: You can specify additional arguments to the nose command used 147 | in this script by just passing arguments to the script. 148 | 149 | For example you can specify the ``-x`` / ``--stop`` argument: 150 | `Stop running tests after the first error or failure` by just doing 151 | 152 | :: 153 | 154 | ./run_tests.sh --stop 155 | 156 | 157 | Each unit-tests files (located under ``fedocal/tests/``) can be called 158 | by alone, allowing easier debugging of the tests. For example: 159 | 160 | :: 161 | 162 | python fedocal/tests/test_week.py 163 | 164 | Similarly as for nose you can also ask that the unit-test stop at the first 165 | error or failure. For example, the command could be: 166 | 167 | :: 168 | 169 | FEDOCAL_CONFIG=tests/fedocal_test.cfg python -m unittest -f -v fedocal.tests.test_week 170 | 171 | 172 | .. note:: In order to have coverage information you might have to install 173 | ``python-coverage`` 174 | 175 | :: 176 | 177 | yum install python-coverage 178 | 179 | 180 | Database changes 181 | ---------------- 182 | 183 | We try to make the database schema as stable as possible, however once in a 184 | while we need to change it to add new features or information. 185 | 186 | 187 | When database changes are made, they should have the corresponding change 188 | handled via `alembic `_. 189 | 190 | 191 | See the `alembic tutorial 192 | `_ for complete 193 | information on how to make a revision to the database schema. 194 | 195 | 196 | The basic idea is to create a revision using (in the top folder): 197 | 198 | :: 199 | 200 | alembic revision -m "" 201 | 202 | Then edit the file generated in alembic/versions/ to add the correct command 203 | for upgrade and downgrade (for example: ``op.add_column``, ``op.drop_column``, 204 | ``op.create_table``, ``op.drop_table``). 205 | 206 | 207 | Translations 208 | ------------ 209 | 210 | Strings are translated using gettext and the Flask-Babel extension. All UI 211 | strings have to be translatable. 212 | 213 | In order to get Fedocal happy even if Flask-Babel is missing, we provide some 214 | wrappers via `fedocal_babel` that must be used instead of requiring directly 215 | Flask-Babel methods. 216 | 217 | In core Python code, you'll have to import the wrapper, and use the `gettext` function:: 218 | 219 | from fedocal.fedocal_babel import gettext 220 | [...] 221 | str = gettext("Translate this!") 222 | 223 | from fedocal.fedocal_babel import gettext 224 | [...] 225 | str = gettext("%(count)s translations found!", count=256) 226 | 227 | In Jinja templates, just use the `_()` function:: 228 | 229 |

{{ _('Fedocal is awesome!') }} 230 |

{{ _('%(users)s in the world use it.', users=2000000) }}

231 | 232 | Once you've added new strings, you'lla have to extract them:: 233 | 234 | pybabel extract -F babel.cfg -o messages.pot fedocal 235 | 236 | And finally, update the `po` file:: 237 | 238 | pybabel update -i messages.pot -d fedocal/translations 239 | 240 | 241 | Troubleshooting 242 | --------------- 243 | 244 | + Login fails in development mode 245 | 246 | The Flask FAS extension requires a secure cookie which ensures that it is 247 | always encrypted during client/server exchanges. 248 | This makes the authentication cookie less likely to be exposed to cookie 249 | theft by eavesdropping. 250 | 251 | You can disable the secure cookie for testing purposes by setting the 252 | configuration key ``FAS_HTTPS_REQUIRED`` to False. 253 | 254 | .. WARNING:: 255 | Do not use this option in production as it causes major security issues 256 | 257 | -------------------------------------------------------------------------------- /tests/test_calendar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2012 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal.model test script 28 | """ 29 | from __future__ import unicode_literals, absolute_import, print_function 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | sys.path.insert(0, os.path.join(os.path.dirname( 36 | os.path.abspath(__file__)), '..')) 37 | 38 | from fedocal.fedocallib import model 39 | from tests import Modeltests 40 | 41 | 42 | class Calendartests(Modeltests): 43 | """ Calendar tests. """ 44 | 45 | def test_init_calendar(self): 46 | """ Test the Calendar init function. """ 47 | obj = model.Calendar( 48 | calendar_name='test_calendar', 49 | calendar_contact='test@example.com', 50 | calendar_description='This is a test calendar', 51 | calendar_editor_group='fi-apprentice', 52 | calendar_admin_group='infrastructure-main2') 53 | obj.save(self.session) 54 | self.session.commit() 55 | self.assertNotEqual(obj, None) 56 | 57 | obj = model.Calendar( 58 | calendar_name='test_calendar2', 59 | calendar_contact='test2@example.com', 60 | calendar_description='This is another test calendar', 61 | calendar_editor_group='packager') 62 | obj.save(self.session) 63 | self.session.commit() 64 | self.assertNotEqual(obj, None) 65 | 66 | obj = model.Calendar( 67 | calendar_name='test_calendar3', 68 | calendar_contact='test3@example.com', 69 | calendar_description='This is the third test calendar') 70 | obj.save(self.session) 71 | self.session.commit() 72 | self.assertNotEqual(obj, None) 73 | 74 | obj = model.Calendar( 75 | calendar_name='test_calendar4', 76 | calendar_contact='test4@example.com', 77 | calendar_description='This is yet another test calendar', 78 | calendar_editor_group='fi-apprentice') 79 | obj.save(self.session) 80 | self.session.commit() 81 | self.assertNotEqual(obj, None) 82 | 83 | obj = model.Calendar( 84 | calendar_name='test_calendar_disabled', 85 | calendar_contact='test4@example.com', 86 | calendar_description='This is a disabled test calendar', 87 | calendar_status='Disabled') 88 | obj.save(self.session) 89 | self.session.commit() 90 | self.assertNotEqual(obj, None) 91 | 92 | def test_repr_calendar(self): 93 | """ Test the Calendar string representation function. """ 94 | self.test_init_calendar() 95 | obj = model.Calendar.by_id(self.session, 'test_calendar') 96 | self.assertNotEqual(obj, None) 97 | self.assertEqual(str(obj), '') 98 | 99 | def test_get_calendar(self): 100 | """ Test the query of a calendar by its name. """ 101 | self.test_init_calendar() 102 | obj = model.Calendar.by_id(self.session, 'test_calendar') 103 | self.assertNotEqual(obj, None) 104 | self.assertEqual(obj.calendar_name, 'test_calendar') 105 | self.assertEqual(obj.calendar_description, 'This is a test calendar') 106 | self.assertEqual(obj.calendar_editor_group, 'fi-apprentice') 107 | self.assertEqual(obj.calendar_admin_group, 'infrastructure-main2') 108 | 109 | def test_get_calendar_inexistant(self): 110 | """ Test by_id query of a non-existant Calendar. """ 111 | self.test_init_calendar() 112 | obj = model.Calendar.by_id(self.session, 'unknonwn') 113 | self.assertEqual(obj, None) 114 | 115 | def test_get_editor_groups(self): 116 | """ Test the Calendar get_editor_groups function. """ 117 | self.test_init_calendar() 118 | obj = model.Calendar.get_editor_groups(self.session, 'test_calendar') 119 | self.assertNotEqual(obj, None) 120 | self.assertEqual(obj, ['fi-apprentice']) 121 | 122 | def test_get_admin_groups(self): 123 | """ Test the Calendar get_admin_groups function. """ 124 | self.test_init_calendar() 125 | obj = model.Calendar.get_admin_groups(self.session, 'test_calendar') 126 | self.assertNotEqual(obj, None) 127 | self.assertEqual(obj, ['infrastructure-main2']) 128 | 129 | # pylint: disable=C0103 130 | def test_get_editor_groups_inexistant_calendar(self): 131 | """ Test the Calendar get_editor_groups function for a non 132 | existant Calendar. 133 | """ 134 | self.test_init_calendar() 135 | obj = model.Calendar.get_editor_groups(self.session, 'unknown') 136 | self.assertNotEqual(obj, None) 137 | self.assertEqual(obj, []) 138 | 139 | # pylint: disable=C0103 140 | def test_get_admin_groups_inexistant_calendar(self): 141 | """ Test the Calendar get_admin_groups function for a non 142 | existant Calendar. 143 | """ 144 | self.test_init_calendar() 145 | obj = model.Calendar.get_admin_groups(self.session, 'unknown') 146 | self.assertNotEqual(obj, None) 147 | self.assertEqual(obj, []) 148 | 149 | # pylint: disable=C0103 150 | def test_get_admin_groups_no_admin_set(self): 151 | """ Test the Calendar get_admin_groups function for a non 152 | existant Calendar. 153 | """ 154 | self.test_init_calendar() 155 | obj = model.Calendar.get_admin_groups(self.session, 'test_calendar3') 156 | self.assertNotEqual(obj, None) 157 | self.assertEqual(obj, []) 158 | 159 | def test_get_all_calendar(self): 160 | """ Test the Calendar get_all function. """ 161 | self.test_init_calendar() 162 | obj = model.Calendar.get_all(self.session) 163 | self.assertNotEqual(obj, None) 164 | self.assertEqual(len(obj), 5) 165 | self.assertEqual(obj[0].calendar_name, 'test_calendar') 166 | self.assertEqual(obj[1].calendar_name, 'test_calendar2') 167 | self.assertEqual(obj[2].calendar_name, 'test_calendar3') 168 | self.assertEqual(obj[3].calendar_name, 'test_calendar4') 169 | self.assertEqual(obj[4].calendar_name, 'test_calendar_disabled') 170 | 171 | def test_get_all_calendar_empty_db(self): 172 | """ Test the Calendar get_all function when the DB is empty. """ 173 | obj = model.Calendar.get_all(self.session) 174 | self.assertNotEqual(obj, None) 175 | self.assertEqual(len(obj), 0) 176 | self.assertEqual(obj, []) 177 | 178 | def test_delete(self): 179 | """ Test the Calendar.delete method. """ 180 | self.test_init_calendar() 181 | obj = model.Calendar.get_all(self.session) 182 | self.assertNotEqual(obj, None) 183 | self.assertEqual(len(obj), 5) 184 | obj = model.Calendar.by_id(self.session, 'test_calendar') 185 | self.assertNotEqual(obj, None) 186 | 187 | obj.delete(self.session) 188 | self.session.commit() 189 | 190 | obj = model.Calendar.get_all(self.session) 191 | self.assertNotEqual(obj, None) 192 | self.assertEqual(len(obj), 4) 193 | obj = model.Calendar.by_id(self.session, 'test_calendar') 194 | self.assertEqual(obj, None) 195 | 196 | 197 | if __name__ == '__main__': 198 | SUITE = unittest.TestLoader().loadTestsFromTestCase(Calendartests) 199 | unittest.TextTestRunner(verbosity=2).run(SUITE) 200 | -------------------------------------------------------------------------------- /fedocal/templates/default/edit_meeting.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block title %}{{ _('Edit meeting') }}{% endblock %} 4 | 5 | {%block tag %}mymeeting{% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 |
11 |

{{ _('Edit meeting "%(name)s"', name=form.meeting_name.data) }}

12 |

{{ _('Please enter your meeting details in the above form.') }}

13 |
14 | 15 |
16 | 17 | {{ render_field_in_row(form.calendar_name) }} 18 | {{ render_field_in_row(form.meeting_name) }} 19 | {{ render_field_in_row(form.meeting_date) }} 20 | {{ render_field_in_row(form.meeting_date_end) }} 21 | {{ render_field_in_row(form.meeting_time_start) }} 22 | {{ render_field_in_row(form.meeting_time_stop) }} 23 | {{ render_field_in_row(form.full_day) }} 24 | {{ render_field_in_row(form.meeting_timezone) }} 25 | {{ render_field_in_row(form.comanager) }} 26 | {% macro get_after_markdown() %} 27 | {{_('Supports the') }} {{ _('Markdown syntax') }} 29 | 30 | {% endmacro %} 31 | {{ render_field_in_row( 32 | form.information, 33 | after=get_after_markdown(), 34 | escape_after=True) }} 35 | {{ render_field_in_row(form.meeting_location) }} 36 |
37 | 38 |

{{ _('Recurring event') }}

39 |

{{ _('If this is a regular meeting this is where you want to set it as so.') }}

40 | 41 | {{ render_field_in_row(form.frequency) }} 42 | {{ render_field_in_row(form.end_repeats, 43 | after=_('Leave empty if there is no end date for the repeat.')) }} 44 |
45 | 46 |

{{ _('Reminder') }}

47 |

{{ _('You may want fedocal to send an email to a mailing list of your choices.') }}

48 |

{{ _('Note that fedocal will send the reminder in your name.') }}

49 | 50 | {{ render_field_in_row(form.remind_when) }} 51 | {{ render_field_in_row(form.reminder_from) }} 52 | {{ render_field_in_row(form.remind_who) }} 53 |
54 | 55 | {{ form.csrf_token }} 56 | 57 |

58 | 59 | {% if meeting.recursion_frequency %} 60 | 61 | {% endif %} 62 | 63 |

64 | 65 |
66 |
67 | {% endblock %} 68 | 69 | {% block jscripts %} 70 | {{ super() }} 71 | 75 | 79 | 233 | {% endblock %} 234 | 235 | -------------------------------------------------------------------------------- /fedocal/templates/default/add_meeting.html: -------------------------------------------------------------------------------- 1 | {% if full %} 2 | {% extends "master.html" %} 3 | 4 | {% block title %}{{ _('Add meeting') }}{% endblock %} 5 | {%block tag %}home{% endblock %} 6 | 7 | {% else %} 8 | {% extends "base_partial.html" %} 9 | {% endif %} 10 | 11 | 12 | {% block content %} 13 | {% from "_formhelpers.html" import render_field_in_row %} 14 | 15 |
16 |
17 |

{{ _('New meeting') }}

18 |

{{ _('Please enter your meeting details in the above form.') }}

19 |
20 | 21 |
23 | 24 | {{ render_field_in_row(form.meeting_name) }} 25 | {{ render_field_in_row(form.meeting_date) }} 26 | {{ render_field_in_row(form.meeting_date_end) }} 27 | {{ render_field_in_row(form.meeting_time_start)}} 28 | {{ render_field_in_row(form.meeting_time_stop)}} 29 | {{ render_field_in_row(form.full_day) }} 30 | {{ render_field_in_row(form.meeting_timezone) }} 31 | {% macro get_after_markdown() %} 32 | {{_('Supports the') }} {{ _('Markdown syntax') }} 34 | 35 | {% endmacro %} 36 | {{ render_field_in_row( 37 | form.information, 38 | after=get_after_markdown(), 39 | escape_after=True) }} 40 | {{ render_field_in_row(form.wiki_link) }} 41 | {{ render_field_in_row(form.meeting_location) }} 42 |
43 | 44 |

{{ _('Recurring event') }}

45 |

{{ _('If this is a regular meeting this is where you want to set it as so.') }}

46 | 47 | {{ render_field_in_row(form.frequency) }} 48 | {{ render_field_in_row(form.end_repeats, after=_('Leave empty if there is no end date for the repeat.')) }} 49 |
50 | 51 |

{{ _('Reminder') }}

52 |

{{ _('You may want fedocal to send an email to a mailing list of your choices.') }}

53 |

{{ _('Note that fedocal will send the reminder in your name.') }}

54 | 55 | {{ render_field_in_row(form.remind_when) }} 56 | {{ render_field_in_row(form.reminder_from) }} 57 | {{ render_field_in_row(form.remind_who) }} 58 |
59 | 60 |

61 | 62 | 63 | {{ form.csrf_token }} 64 |

65 |
66 |
67 | {% endblock %} 68 | 69 | {% block jscripts %} 70 | {{ super() }} 71 | 75 | 79 | 105 | 132 | 150 | 174 | 238 | {% endblock %} 239 | -------------------------------------------------------------------------------- /tests/test_flask_extras.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2014 - Copyright Pierre-Yves Chibon 6 | Author: Pierre-Yves Chibon 7 | 8 | Distributed under License GPLv3 or later 9 | You can find a copy of this license on the website 10 | http://www.gnu.org/licenses/gpl.html 11 | 12 | This program is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 25 | MA 02110-1301, USA. 26 | 27 | fedocal's flask application tests script 28 | - for special corner case to test 29 | """ 30 | from __future__ import unicode_literals, absolute_import, print_function 31 | 32 | import flask 33 | import logging 34 | import unittest 35 | import sys 36 | import os 37 | 38 | from datetime import date 39 | from datetime import time 40 | from datetime import timedelta 41 | 42 | sys.path.insert(0, os.path.join(os.path.dirname( 43 | os.path.abspath(__file__)), '..')) 44 | 45 | import fedocal 46 | import fedocal.fedocallib as fedocallib 47 | import fedocal.fedocallib.model as model 48 | from tests import (Modeltests, FakeUser, user_set, TODAY) 49 | 50 | 51 | # pylint: disable=E1103 52 | class ExtrasFlasktests(Modeltests): 53 | """ Extras Flask application tests. """ 54 | 55 | def __setup_calendar(self): 56 | """ Set up basic calendar information. """ 57 | obj = model.Calendar( 58 | calendar_name='test_calendar', 59 | calendar_contact='test@example.com', 60 | calendar_description='This is a test calendar', 61 | calendar_editor_group='fi-apprentice', 62 | calendar_admin_group='infrastructure-main2') 63 | obj.save(self.session) 64 | self.session.commit() 65 | self.assertNotEqual(obj, None) 66 | 67 | def setUp(self): 68 | """ Set up the environnment, ran before every tests. """ 69 | super(ExtrasFlasktests, self).setUp() 70 | 71 | fedocal.APP.config['TESTING'] = True 72 | fedocal.APP.debug = True 73 | fedocal.APP.logger.handlers = [] 74 | fedocal.APP.logger.setLevel(logging.CRITICAL) 75 | fedocal.SESSION = self.session 76 | self.app = fedocal.APP.test_client() 77 | 78 | def test_start_date_edit_meeting_form(self): 79 | """ Test the content of the start_date in the edit meeting form. 80 | """ 81 | self.__setup_calendar() 82 | 83 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 84 | # Create a meeting 85 | obj = model.Meeting( # id:1 86 | meeting_name='recursive meeting', 87 | meeting_date=TODAY, 88 | meeting_date_end=TODAY, 89 | meeting_time_start=time(19, 50), 90 | meeting_time_stop=time(20, 50), 91 | meeting_information='This is a test meeting', 92 | calendar_name='test_calendar', 93 | recursion_frequency=14, 94 | recursion_ends=TODAY + timedelta(days=90)) 95 | self.session.add(obj) 96 | self.session.flush() 97 | obj.add_manager(self.session, 'pingou,') 98 | obj.save(self.session) 99 | self.session.commit() 100 | self.assertNotEqual(obj, None) 101 | 102 | user = FakeUser(['fi-apprentice'], username='pingou') 103 | with user_set(fedocal.APP, user): 104 | output = self.app.get('/meeting/edit/1/') 105 | self.assertEqual(output.status_code, 200) 106 | output_text = output.get_data(as_text=True) 107 | 108 | next_date = TODAY + timedelta(days=14) 109 | if date.today() <= TODAY: 110 | next_date = TODAY 111 | 112 | # If no date is specified, it returns the next occurence 113 | self.assertIn( 114 | '' % (next_date), output_text 116 | ) 117 | 118 | # If a date in the future is specified, return the next occurence 119 | # for this date 120 | url = '/meeting/edit/1/?from_date=%s' % ( 121 | TODAY + timedelta(days=20)) 122 | output2 = self.app.get(url) 123 | self.assertEqual(output2.status_code, 200) 124 | output2_text = output2.get_data(as_text=True) 125 | 126 | self.assertIn( 127 | '' % (TODAY + timedelta(days=28)), output2_text 129 | ) 130 | 131 | # If an exact date in the future is specified, return that date 132 | url = '/meeting/edit/1/?from_date=%s' % ( 133 | TODAY + timedelta(days=14)) 134 | output2 = self.app.get(url) 135 | self.assertEqual(output2.status_code, 200) 136 | output2_text = output2.get_data(as_text=True) 137 | 138 | self.assertIn( 139 | '' % (TODAY + timedelta(days=14)), output2_text 141 | ) 142 | 143 | # If an old date in the future is specified, return the first date 144 | output2 = self.app.get('/meeting/edit/1/?from_date=2000-01-01') 145 | self.assertEqual(output2.status_code, 200) 146 | output2_text = output2.get_data(as_text=True) 147 | 148 | self.assertIn( 149 | '' % (TODAY), output2_text 151 | ) 152 | 153 | def test_start_date_delete_meeting_form(self): 154 | """ Test the content of the start_date in the delete meeting form. 155 | """ 156 | self.__setup_calendar() 157 | 158 | calendar = model.Calendar.by_id(self.session, 'test_calendar') 159 | # Create a meeting 160 | obj = model.Meeting( # id:1 161 | meeting_name='recursive meeting', 162 | meeting_date=TODAY, 163 | meeting_date_end=TODAY, 164 | meeting_time_start=time(19, 50), 165 | meeting_time_stop=time(20, 50), 166 | meeting_information='This is a test meeting', 167 | calendar_name='test_calendar', 168 | recursion_frequency=14, 169 | recursion_ends=TODAY + timedelta(days=90)) 170 | self.session.add(obj) 171 | self.session.flush() 172 | obj.add_manager(self.session, 'pingou,') 173 | obj.save(self.session) 174 | self.session.commit() 175 | self.assertNotEqual(obj, None) 176 | 177 | user = FakeUser(['fi-apprentice'], username='pingou') 178 | with user_set(fedocal.APP, user): 179 | output = self.app.get('/meeting/delete/1/') 180 | self.assertEqual(output.status_code, 200) 181 | output_text = output.get_data(as_text=True) 182 | 183 | next_date = TODAY + timedelta(days=14) 184 | if date.today() <= TODAY: 185 | next_date = TODAY 186 | 187 | # If no date is specified, it returns the next occurence 188 | self.assertIn('
  • Date: %s
  • ' % next_date, output_text) 189 | 190 | # If a date in the future is specified, return the next occurence 191 | # for this date 192 | url = '/meeting/delete/1/?from_date=%s' % ( 193 | TODAY + timedelta(days=20)) 194 | output2 = self.app.get(url) 195 | self.assertEqual(output2.status_code, 200) 196 | output_text = output2.get_data(as_text=True) 197 | 198 | self.assertIn( 199 | '
  • Date: %s
  • ' % (TODAY + timedelta(days=28)), 200 | output_text 201 | ) 202 | 203 | # If an exact date in the future is specified, return that date 204 | url = '/meeting/delete/1/?from_date=%s' % ( 205 | TODAY + timedelta(days=14)) 206 | output2 = self.app.get(url) 207 | self.assertEqual(output2.status_code, 200) 208 | output_text = output2.get_data(as_text=True) 209 | 210 | self.assertIn('
  • Date: %s
  • ' % (TODAY + timedelta(days=14)), output_text) 211 | 212 | # If an old date in the future is specified, return the first date 213 | output2 = self.app.get('/meeting/delete/1/?from_date=2000-01-01') 214 | self.assertEqual(output2.status_code, 200) 215 | output_text = output2.get_data(as_text=True) 216 | 217 | self.assertTrue( 218 | '
  • Date: %s
  • ' % (TODAY), output_text 219 | ) 220 | 221 | 222 | if __name__ == '__main__': 223 | SUITE = unittest.TestLoader().loadTestsFromTestCase(ExtrasFlasktests) 224 | unittest.TextTestRunner(verbosity=2).run(SUITE) 225 | --------------------------------------------------------------------------------