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()