diff --git a/calsocial/__init__.py b/calsocial/__init__.py index 9e24926..0c172bf 100644 --- a/calsocial/__init__.py +++ b/calsocial/__init__.py @@ -25,6 +25,8 @@ 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.utils import RoutedMixin + def get_locale(): """Locale selector @@ -53,22 +55,7 @@ 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): # pylint: disable=missing-docstring - setattr(func, 'routing', (args, kwargs)) - - return func - - return decorator - - -class CalendarSocialApp(Flask): +class CalendarSocialApp(Flask, RoutedMixin): """The Calendar.social app """ @@ -101,18 +88,7 @@ class CalendarSocialApp(Flask): self.context_processor(template_vars) - for attr_name in self.__dir__(): - attr = getattr(self, attr_name) - - if not callable(attr): - continue - - args, kwargs = getattr(attr, 'routing', (None, None)) - - if args is None: - continue - - self.route(*args, **kwargs)(attr) + RoutedMixin.register_routes(self) self.before_request(self.goto_first_steps) @@ -165,7 +141,7 @@ class CalendarSocialApp(Flask): return GregorianCalendar(timestamp.timestamp()) - @route('/about') + @RoutedMixin.route('/about') def about(self): """View for the about page """ @@ -190,7 +166,7 @@ class CalendarSocialApp(Flask): user_count=user_count, event_count=event_count) - @route('/') + @RoutedMixin.route('/') def hello(self): """View for the main page @@ -206,7 +182,7 @@ class CalendarSocialApp(Flask): return render_template('index.html', calendar=calendar, user_only=True) @staticmethod - @route('/register', methods=['POST', 'GET']) + @RoutedMixin.route('/register', methods=['POST', 'GET']) def register(): """View for user registration @@ -236,7 +212,7 @@ class CalendarSocialApp(Flask): return render_template('registration.html', form=form) @staticmethod - @route('/new-event', methods=['GET', 'POST']) + @RoutedMixin.route('/new-event', methods=['GET', 'POST']) @login_required def new_event(): """View for creating a new event @@ -261,7 +237,7 @@ class CalendarSocialApp(Flask): return render_template('event-edit.html', form=form) @staticmethod - @route('/settings', methods=['GET', 'POST']) + @RoutedMixin.route('/settings', methods=['GET', 'POST']) @login_required def settings(): """View for user settings @@ -282,7 +258,7 @@ class CalendarSocialApp(Flask): return render_template('user-settings.html', form=form) @staticmethod - @route('/event/', methods=['GET', 'POST']) + @RoutedMixin.route('/event/', methods=['GET', 'POST']) def event_details(event_uuid): """View to display event details """ @@ -309,7 +285,7 @@ class CalendarSocialApp(Flask): return render_template('event-details.html', event=event, form=form) @staticmethod - @route('/profile/@') + @RoutedMixin.route('/profile/@') def display_profile(username): """View to display profile details """ @@ -324,7 +300,7 @@ class CalendarSocialApp(Flask): return render_template('profile-details.html', profile=profile) @staticmethod - @route('/profile/@/follow') + @RoutedMixin.route('/profile/@/follow') @login_required def follow_user(username): """View for following a user @@ -345,7 +321,7 @@ class CalendarSocialApp(Flask): return redirect(url_for('display_profile', username=username)) @staticmethod - @route('/notifications') + @RoutedMixin.route('/notifications') def notifications(): """View to list the notifications for the current user """ @@ -360,7 +336,7 @@ class CalendarSocialApp(Flask): return render_template('notifications.html', notifs=notifs) @staticmethod - @route('/accept/') + @RoutedMixin.route('/accept/') def accept_invite(invite_id): """View to accept an invitation """ @@ -390,7 +366,7 @@ class CalendarSocialApp(Flask): return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) @staticmethod - @route('/first-steps', methods=['GET', 'POST']) + @RoutedMixin.route('/first-steps', methods=['GET', 'POST']) @login_required def first_steps(): """View to set up a new registrant’s profile @@ -417,7 +393,7 @@ class CalendarSocialApp(Flask): return render_template('first-steps.html', form=form) @staticmethod - @route('/edit-profile', methods=['GET', 'POST']) + @RoutedMixin.route('/edit-profile', methods=['GET', 'POST']) @login_required def edit_profile(): """View for editing one’s profile @@ -438,7 +414,7 @@ class CalendarSocialApp(Flask): return render_template('profile-edit.html', form=form) @staticmethod - @route('/all-events') + @RoutedMixin.route('/all-events') def all_events(): """View for listing all available events """ @@ -455,7 +431,7 @@ class CalendarSocialApp(Flask): return render_template('index.html', calendar=calendar, user_only=False) @staticmethod - @route('/follow-requests') + @RoutedMixin.route('/follow-requests') @login_required def follow_requests(): """View for listing follow requests @@ -470,7 +446,7 @@ class CalendarSocialApp(Flask): return render_template('follow-requests.html', requests=requests) @staticmethod - @route('/follow-request//accept') + @RoutedMixin.route('/follow-request//accept') @login_required def accept_follow(follower_id): """View for accepting a follow request diff --git a/calsocial/utils.py b/calsocial/utils.py index 2127d61..09b22af 100644 --- a/calsocial/utils.py +++ b/calsocial/utils.py @@ -68,3 +68,51 @@ def force_locale(locale): babel.locale_selector_func = orig_locale_selector_func for key, value in orig_attrs.items(): setattr(ctx, key, value) + + +class RoutedMixin: + """Mixin to lazily register class methods as routes + + Works both for `Flask` and `Blueprint` objects. + + Example:: + + class MyBlueprint(Blueprint, RoutedMixin): + def __init__(self, *args, **kwargs): + do_whatever_you_like() + + RoutedMixin.register_routes(self) + + @RoutedMixin.route('/') + def index(self): + return 'Hello, World!' + """ + + def register_routes(self): + for attr_name in self.__dir__(): + attr = getattr(self, attr_name) + + if not callable(attr): + continue + + args, kwargs = getattr(attr, 'routing', (None, None)) + + if args is None: + continue + + self.route(*args, **kwargs)(attr) + + @staticmethod + 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): # pylint: disable=missing-docstring + setattr(func, 'routing', (args, kwargs)) + + return func + + return decorator