diff --git a/calsocial/__init__.py b/calsocial/__init__.py
index 760bd30..b8db121 100644
--- a/calsocial/__init__.py
+++ b/calsocial/__init__.py
@@ -14,6 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+"""Main module for the Calendar.social app
+"""
+
from datetime import datetime
from functools import wraps
import os
@@ -40,6 +43,9 @@ def get_locale():
def template_vars():
+ """Function to inject global template variables
+ """
+
now = datetime.utcnow()
return {
@@ -50,6 +56,12 @@ def template_vars():
def route(*args, **kwargs):
+ """Mark a function as a future route
+
+ Such functions will be iterated over when the application is initialised. ``*args`` and
+ ``**kwargs`` will be passed verbatim to `Flask.route()`.
+ """
+
def decorator(func):
setattr(func, 'routing', (args, kwargs))
@@ -59,6 +71,9 @@ def route(*args, **kwargs):
class CalendarSocialApp(Flask):
+ """The Calendar.social app
+ """
+
def __init__(self, name, config=None):
from .models import db, User, Role
from .security import security
@@ -93,6 +108,12 @@ class CalendarSocialApp(Flask):
@route('/')
def hello(self):
+ """View for the main page
+
+ This will display a welcome message for users not logged in; for others, their main
+ calendar view is displayed.
+ """
+
from .calendar_system.gregorian import GregorianCalendar
if not current_user.is_authenticated:
@@ -109,6 +130,12 @@ class CalendarSocialApp(Flask):
@route('/register', methods=['POST', 'GET'])
def register(self):
+ """View for user registration
+
+ If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the
+ registration disabled template. Otherwise, it performs user registration.
+ """
+
if not current_app.config['REGISTRATION_ENABLED']:
return render_template('registration-disabled.html')
@@ -132,6 +159,11 @@ class CalendarSocialApp(Flask):
@route('/new-event', methods=['GET', 'POST'])
@login_required
def new_event(self):
+ """View for creating a new event
+
+ This presents a form to the user that allows entering event details.
+ """
+
from .forms import EventForm
from .models import db, Event
diff --git a/calsocial/__main__.py b/calsocial/__main__.py
index 0206b89..26c561c 100644
--- a/calsocial/__main__.py
+++ b/calsocial/__main__.py
@@ -1,3 +1,6 @@
+"""Main module for Calendar.social so the app can be run directly.
+"""
+
from calsocial import CalendarSocialApp
diff --git a/calsocial/calendar_system/__init__.py b/calsocial/calendar_system/__init__.py
index a5dda87..dba786f 100644
--- a/calsocial/calendar_system/__init__.py
+++ b/calsocial/calendar_system/__init__.py
@@ -1,15 +1,48 @@
+# Calendar.social
+# Copyright (C) 2018 Gergely Polonkai
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Calendar system base definition for Calendar.social
+"""
+
class CalendarSystem:
+ """Base class for calendar systems
+ """
+
__has_months__ = False
__has_weeks__ = False
@property
def day_names(self):
+ """An iterable of day names
+ """
+
raise NotImplementedError()
@property
def month(self):
+ """The name of the current month, if applicable
+
+ It may include the year.
+ """
+
raise NotImplementedError()
@property
def days(self):
+ """An iterable of days to be displayed on the month view, if applicable
+ """
+
raise NotImplementedError()
diff --git a/calsocial/calendar_system/gregorian.py b/calsocial/calendar_system/gregorian.py
index 1113761..0705269 100644
--- a/calsocial/calendar_system/gregorian.py
+++ b/calsocial/calendar_system/gregorian.py
@@ -1,3 +1,22 @@
+# Calendar.social
+# Copyright (C) 2018 Gergely Polonkai
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Gregorian calendar system for Calendar.social
+"""
+
from datetime import datetime, timedelta
from functools import wraps
@@ -7,6 +26,9 @@ from . import CalendarSystem
def to_timestamp(func):
+ """Decorator that converts the return value of a function from `datetime` to a UNIX timestamp
+ """
+
@wraps(func)
def decorator(*args, **kwargs):
return func(*args, **kwargs).timestamp()
@@ -15,6 +37,9 @@ def to_timestamp(func):
class GregorianCalendar(CalendarSystem):
+ """Gregorian calendar system for Calendar.social
+ """
+
__name__ = _('Gregorian')
__has_months__ = True
@@ -75,15 +100,24 @@ class GregorianCalendar(CalendarSystem):
@property
@to_timestamp
def prev_year(self):
+ """Returns the timestamp of the same date in the previous year
+ """
+
return self.timestamp.replace(year=self.timestamp.year - 1)
@property
def prev_year_year(self):
+ """The number of the previous year
+ """
+
return self.timestamp.replace(year=self.timestamp.year - 1).year
@property
@to_timestamp
def prev_month(self):
+ """Returns the timestamp of the same day in the previous month
+ """
+
if self.timestamp.month == 1:
return self.prev_year.replace(month=12)
@@ -91,6 +125,9 @@ class GregorianCalendar(CalendarSystem):
@property
def prev_month_name(self):
+ """The name of the previous month
+ """
+
if self.timestamp.month == 1:
timestamp = self.prev_year.replace(month=12)
else:
@@ -101,6 +138,9 @@ class GregorianCalendar(CalendarSystem):
@property
@to_timestamp
def next_month(self):
+ """Returns the timestamp of the same day in the next month
+ """
+
if self.timestamp.month == 12:
return self.next_year.replace(month=1)
@@ -108,6 +148,9 @@ class GregorianCalendar(CalendarSystem):
@property
def next_month_name(self):
+ """The name of the next month
+ """
+
if self.timestamp.month == 12:
timestamp = self.prev_year.replace(month=1)
else:
@@ -118,14 +161,23 @@ class GregorianCalendar(CalendarSystem):
@property
@to_timestamp
def next_year(self):
+ """Returns the timestamp of the same date in the next year
+ """
+
return self.timestamp.replace(year=self.timestamp.year + 1)
@property
def next_year_year(self):
+ """The number of the next year
+ """
+
return self.timestamp.replace(year=self.timestamp.year + 1).year
@property
def has_today(self):
+ """``True`` if the set month holds today’s date
+ """
+
now = datetime.utcnow()
month_start_timestamp = self.timestamp.replace(day=1,
hour=0,
@@ -144,6 +196,9 @@ class GregorianCalendar(CalendarSystem):
@staticmethod
def day_events(date, user=None):
+ """Returns all events for a given day
+ """
+
from ..models import Event
events = Event.query
diff --git a/calsocial/config_dev.py b/calsocial/config_dev.py
index 51f0fff..5cc4d10 100644
--- a/calsocial/config_dev.py
+++ b/calsocial/config_dev.py
@@ -1,5 +1,9 @@
+"""Configuration file for the development environment
+"""
+
DEBUG = True
ENV = 'dev'
+#: If ``True``, registration on the site is enabled.
REGISTRATION_ENABLED = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///local.db'
diff --git a/calsocial/forms.py b/calsocial/forms.py
index dad0bd9..a5973eb 100644
--- a/calsocial/forms.py
+++ b/calsocial/forms.py
@@ -1,3 +1,22 @@
+# Calendar.social
+# Copyright (C) 2018 Gergely Polonkai
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Forms for Calendar.social
+"""
+
from flask_babelex import lazy_gettext as _
from flask_wtf import FlaskForm
import pytz
@@ -8,17 +27,32 @@ from wtforms.widgets import TextArea
class RegistrationForm(FlaskForm):
+ """Registration form
+ """
+
username = StringField(_('Username'), validators=[DataRequired()])
email = StringField(_('Email address'), validators=[Email()])
password = PasswordField(_('Password'), validators=[DataRequired()])
password_retype = PasswordField(_('Password, once more'), validators=[DataRequired()])
def validate_password_retype(self, field):
+ """Validate the ``password_retype`` field
+
+ Its value must match the ``password`` field’s.
+ """
+
if field.data != self.password.data:
raise ValidationError(_('The two passwords must match!'))
class TimezoneField(SelectField):
+ """Field for selecting a time zone
+
+ Note: this field overrides whatever is passed to the ``choices`` parameter, and fills
+ ``choices`` with the common timezones of pytz. In every other aspects, it behaves exactly
+ like `SelectField`.
+ """
+
def __init__(self, *args, **kwargs):
kwargs.update({
'choices': [
@@ -44,6 +78,9 @@ class TimezoneField(SelectField):
@staticmethod
def is_pytz_instance(value):
+ """Check if ``value`` is a valid pytz time zone instance
+ """
+
return value is pytz.UTC or isinstance(value, pytz.tzinfo.BaseTzInfo)
def process_data(self, value):
@@ -68,6 +105,9 @@ class TimezoneField(SelectField):
class EventForm(FlaskForm):
+ """Form for event creation/editing
+ """
+
title = StringField(_('Title'), validators=[DataRequired()])
time_zone = TimezoneField(_('Time zone'), validators=[DataRequired()])
start_time = DateTimeField(_('Start time'), validators=[DataRequired()])
@@ -76,6 +116,9 @@ class EventForm(FlaskForm):
description = StringField(_('Description'), widget=TextArea())
def populate_obj(self, obj):
+ """Populate ``obj`` with event data
+ """
+
FlaskForm.populate_obj(self, obj)
timezone = self.time_zone.data
@@ -85,5 +128,10 @@ class EventForm(FlaskForm):
obj.end_time = timezone.localize(self.end_time.data).astimezone(pytz.utc)
def validate_end_time(self, field):
+ """Validate the ``end_time`` field
+
+ Its value must be later than the value of the ``start_time`` field.
+ """
+
if field.data < self.start_time.data:
raise ValidationError(_('End time must be later than start time!'))
diff --git a/calsocial/models.py b/calsocial/models.py
index da48af9..3a813e9 100644
--- a/calsocial/models.py
+++ b/calsocial/models.py
@@ -1,3 +1,22 @@
+# Calendar.social
+# Copyright (C) 2018 Gergely Polonkai
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Database models for Calendar.social
+"""
+
from datetime import datetime
from flask_security import UserMixin, RoleMixin
@@ -11,6 +30,9 @@ users_roles = db.Table(
class User(db.Model, UserMixin):
+ """Database model for users
+ """
+
__tablename__ = 'users'
id = db.Column(db.Integer(), primary_key=True)
@@ -42,6 +64,9 @@ class User(db.Model, UserMixin):
class Role(db.Model, RoleMixin):
+ """Database model for roles
+ """
+
__tablename__ = 'roles'
id = db.Column(db.Integer(), primary_key=True)
@@ -56,6 +81,9 @@ class Role(db.Model, RoleMixin):
class Event(db.Model):
+ """Database model for events
+ """
+
__tablename__ = 'events'
id = db.Column(db.Integer(), primary_key=True)
@@ -70,10 +98,10 @@ class Event(db.Model):
#: The time zone to be used for `start_time` and `end_time`
time_zone = db.Column(db.String(length=80), nullable=False)
- #: The starting timestamp of the event
+ #: The starting timestamp of the event. It is in the UTC time zone
start_time = db.Column(db.DateTime(), nullable=False)
- #: The ending timestamp of the event
+ #: The ending timestamp of the event. It is in the UTC time zone
end_time = db.Column(db.DateTime(), nullable=False)
#: If `True`, the event is a whole-day event
@@ -89,10 +117,16 @@ class Event(db.Model):
@property
def start_time_tz(self):
+ """The same timestamp as `start_time`, but in the time zone specified by `time_zone`.
+ """
+
return self.__as_tz(self.start_time)
@property
def end_time_tz(self):
+ """The same timestamp as `end_time`, but in the time zone specified by `time_zone`.
+ """
+
return self.__as_tz(self.end_time)
def __repr__(self):
diff --git a/calsocial/security.py b/calsocial/security.py
index 56ff78e..39bd35b 100644
--- a/calsocial/security.py
+++ b/calsocial/security.py
@@ -1,3 +1,22 @@
+# Calendar.social
+# Copyright (C) 2018 Gergely Polonkai
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Security related things for Calendar.social
+"""
+
from flask_security import Security
security = Security()