# 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 . """Main module for the Calendar.social app """ from datetime import datetime import os from warnings import warn from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \ url_for from flask_babelex import Babel, get_locale as babel_get_locale from flask_security import SQLAlchemyUserDatastore, current_user, login_required from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from calsocial.account import AccountBlueprint from calsocial.cache import CachedSessionInterface, cache from calsocial.utils import RoutedMixin def get_locale(): """Locale selector Selects the best locale based on values sent by the browser. """ supported_languages = ['en', 'hu'] if 'l' in request.args and request.args['l'].lower() in supported_languages: return request.args['l'].lower() return request.accept_languages.best_match(supported_languages) def template_vars(): """Function to inject global template variables """ now = datetime.utcnow() return { 'lang': babel_get_locale().language, 'now': now, 'now_ts': now.timestamp(), } class CalendarSocialApp(Flask, RoutedMixin): """The Calendar.social app """ def __init__(self, name, config=None): # pylint: disable=too-many-locals from .forms import LoginForm from .models import db, User, Role from .security import security, AnonymousUser Flask.__init__(self, name) self.session_interface = CachedSessionInterface() self._timezone = None config_name = os.environ.get('FLASK_ENV', config or 'development') self.config.from_pyfile(f'config_{config_name}.py', True) # Make sure we look up users both by their usernames and email addresses self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html' # The builtin avatars to use self.config['BUILTIN_AVATARS'] = ( 'doctor', 'engineer', 'scientist', 'statistician', 'user', 'whoami', ) self.jinja_env.policies['ext.i18n.trimmed'] = True # pylint: disable=no-member db.init_app(self) cache.init_app(self) babel = Babel(app=self) babel.localeselector(get_locale) user_store = SQLAlchemyUserDatastore(db, User, Role) security.init_app(self, datastore=user_store, anonymous_user=AnonymousUser, login_form=LoginForm) self.context_processor(template_vars) RoutedMixin.register_routes(self) AccountBlueprint().init_app(self, '/accounts/') self.before_request(self.goto_first_steps) @staticmethod def goto_first_steps(): """Check if the current user has a profile and if not, redirect to the first steps page """ if current_user.is_authenticated and \ not current_user.profile and \ request.endpoint != 'account.first_steps': return redirect(url_for('account.first_steps')) return None @property def timezone(self): """The default time zone of the app """ from pytz import timezone, utc from pytz.exceptions import UnknownTimeZoneError if not has_app_context(): return utc if not self._timezone: timezone_str = current_app.config.get('DEFAULT_TIMEZONE', 'UTC') try: self._timezone = timezone(timezone_str) except UnknownTimeZoneError: warn(f'Timezone of {self} (or the default timezone) "{timezone_str}" is invalid') self._timezone = utc return self._timezone @property def instance_admin(self): """The admin user of this instance """ from calsocial.models import AppState, User if not has_app_context(): return None admin_id = AppState['instance_admin'] try: admin_id = int(admin_id) except (TypeError, ValueError): warn(f'Instance admin is not set correctly (value is {admin_id})') return None try: return User.query.filter(User.id == admin_id).one() except NoResultFound: warn(f'Instance admin is not set correctly (value is {admin_id})') return None @staticmethod def _current_calendar(): from .calendar_system.gregorian import GregorianCalendar try: timestamp = datetime.fromtimestamp(float(request.args.get('date'))) except TypeError: timestamp = datetime.utcnow() return GregorianCalendar(timestamp.timestamp()) @RoutedMixin.route('/about') def about(self): """View for the about page """ from .models import User, Event calendar = self._current_calendar() if not current_user.is_authenticated: login_form_class = current_app.extensions['security'].login_form login_form = login_form_class() else: login_form = None user_count = User.query.count() event_count = Event.query.count() admin_user = current_app.instance_admin admin_profile = None if admin_user is None else admin_user.profile return render_template('welcome.html', calendar=calendar, user_only=False, login_form=login_form, user_count=user_count, event_count=event_count, admin_profile=admin_profile) @RoutedMixin.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. """ calendar = self._current_calendar() if not current_user.is_authenticated: return self.about() return render_template('index.html', calendar=calendar, user_only=True) @staticmethod @RoutedMixin.route('/new-event', methods=['GET', 'POST']) @login_required def new_event(): """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 form = EventForm() if form.validate_on_submit(): event = Event(profile=current_user.profile) form.populate_obj(event) db.session.add(event) db.session.commit() return redirect(url_for('hello')) return render_template('event-edit.html', form=form) @staticmethod @RoutedMixin.route('/event/', methods=['GET', 'POST']) def event_details(event_uuid): """View to display event details """ from .forms import InviteForm from .models import db, Event try: event = Event.query.filter(Event.event_uuid == event_uuid).one() except NoResultFound: abort(404) except MultipleResultsFound: abort(500) form = InviteForm(event) if form.validate_on_submit(): event.invite(current_user.profile, invitee=form.invitee.data) db.session.commit() return redirect(url_for('event_details', event_uuid=event.event_uuid)) return render_template('event-details.html', event=event, form=form) @staticmethod @RoutedMixin.route('/profile/@') def display_profile(username): """View to display profile details """ from .models import Profile, User try: profile = Profile.query.join(User).filter(User.username == username).one() except NoResultFound: abort(404) return render_template('profile-details.html', profile=profile) @staticmethod @RoutedMixin.route('/profile/@/follow') @login_required def follow_user(username): """View for following a user """ from .models import db, Profile, User try: profile = Profile.query.join(User).filter(User.username == username).one() except NoResultFound: abort(404) if profile.user != current_user: profile.follow(follower=current_user.profile) db.session.commit() return redirect(url_for('display_profile', username=username)) @staticmethod @RoutedMixin.route('/accept/') def accept_invite(invite_id): """View to accept an invitation """ from .models import db, Invitation, Response, ResponseType invitation = Invitation.query.get_or_404(invite_id) if invitation.invitee != current_user.profile: abort(403) try: Response.query \ .filter(Response.event == invitation.event) \ .filter(Response.profile == current_user.profile) \ .one() except NoResultFound: response = Response(profile=current_user.profile, event=invitation.event, invitation=invitation, response=ResponseType.going) db.session.add(response) db.session.commit() except MultipleResultsFound: pass return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) @staticmethod @RoutedMixin.route('/all-events') def all_events(): """View for listing all available events """ from .calendar_system.gregorian import GregorianCalendar try: timestamp = datetime.fromtimestamp(float(request.args.get('date'))) except TypeError: timestamp = datetime.utcnow() calendar = GregorianCalendar(timestamp.timestamp()) return render_template('index.html', calendar=calendar, user_only=False) app = CalendarSocialApp(__name__)