Compare commits
	
		
			13 Commits
		
	
	
		
			test-cover
			...
			groups
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 74530347e2 | |||
| eaf71d4ce6 | |||
| 7cd2156cfc | |||
| b9c037f914 | |||
| 029d29ffb1 | |||
| 4b1fff6544 | |||
| 490474b2d6 | |||
| bc67e692e0 | |||
| 1e1e085ba4 | |||
| 5996ae7079 | |||
| 3e5d8ee4d5 | |||
| c0c38ccb52 | |||
| c40e776036 | 
							
								
								
									
										1
									
								
								.env.testing
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.testing
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| FLASK_ENV=testing | ||||
| @@ -19,14 +19,17 @@ | ||||
|  | ||||
| from datetime import datetime | ||||
| import os | ||||
| from warnings import warn | ||||
|  | ||||
| from flask import Flask, abort, current_app, redirect, render_template, request, url_for | ||||
| 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.group import GroupBlueprint | ||||
| from calsocial.utils import RoutedMixin | ||||
|  | ||||
|  | ||||
| @@ -72,12 +75,22 @@ class CalendarSocialApp(Flask, RoutedMixin): | ||||
|  | ||||
|         self._timezone = None | ||||
|  | ||||
|         config_name = os.environ.get('ENV', config or 'development') | ||||
|         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) | ||||
| @@ -97,6 +110,7 @@ class CalendarSocialApp(Flask, RoutedMixin): | ||||
|         RoutedMixin.register_routes(self) | ||||
|  | ||||
|         AccountBlueprint().init_app(self, '/accounts/') | ||||
|         GroupBlueprint().init_app(self, '/groups/') | ||||
|  | ||||
|         self.before_request(self.goto_first_steps) | ||||
|  | ||||
| @@ -117,9 +131,6 @@ class CalendarSocialApp(Flask, RoutedMixin): | ||||
|         """The default time zone of the app | ||||
|         """ | ||||
|  | ||||
|         from warnings import warn | ||||
|  | ||||
|         from flask import has_app_context | ||||
|         from pytz import timezone, utc | ||||
|         from pytz.exceptions import UnknownTimeZoneError | ||||
|  | ||||
| @@ -138,6 +149,32 @@ class CalendarSocialApp(Flask, RoutedMixin): | ||||
|  | ||||
|         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 | ||||
| @@ -166,13 +203,16 @@ class CalendarSocialApp(Flask, RoutedMixin): | ||||
|  | ||||
|         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) | ||||
|                                event_count=event_count, | ||||
|                                admin_profile=admin_profile) | ||||
|  | ||||
|     @RoutedMixin.route('/') | ||||
|     def hello(self): | ||||
|   | ||||
							
								
								
									
										60
									
								
								calsocial/app_state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								calsocial/app_state.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| """Metaclass for storing and accessing app state | ||||
| """ | ||||
|  | ||||
| def get_state_base(self, key): | ||||
|     """Method to get a key from the state store | ||||
|     """ | ||||
|  | ||||
|     return self.__get_state__(key) | ||||
|  | ||||
| def set_state_base(self, key, value): | ||||
|     """Method to set a key/value in the state store | ||||
|     """ | ||||
|  | ||||
|     self.__set_state__(key, str(value)) | ||||
|  | ||||
| def set_default_base(self, key, value): | ||||
|     """Method to set the default value of a key in the state store | ||||
|  | ||||
|     If key is already in the state store, this method is a no-op. | ||||
|     """ | ||||
|  | ||||
|     self.__set_state_default__(key, str(value)) | ||||
|  | ||||
|  | ||||
| def app_state_base(klass): | ||||
|     """Base class creator for AppStateMeta types | ||||
|  | ||||
|     :param klass: the class to extend | ||||
|     :type klass: type | ||||
|     :returns: a new class extending ``klass`` | ||||
|     :rtype: type | ||||
|     """ | ||||
|  | ||||
|     # Construct the meta class based on the metaclass of ``klass`` | ||||
|     metaclass = type( | ||||
|         klass.__name__ + 'BaseMeta', | ||||
|         (type(klass),), | ||||
|         { | ||||
|             '__getitem__': get_state_base, | ||||
|             '__setitem__': set_state_base, | ||||
|             'setdefault': set_default_base, | ||||
|         }) | ||||
|  | ||||
|     return metaclass(klass.__name__ + 'Base', (klass,), {'__abstract__': True}) | ||||
| @@ -18,24 +18,12 @@ | ||||
| """ | ||||
|  | ||||
| from datetime import datetime, timedelta | ||||
| from functools import wraps | ||||
|  | ||||
| from flask_babelex import lazy_gettext as _ | ||||
|  | ||||
| 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() | ||||
|  | ||||
|     return _decorator | ||||
|  | ||||
|  | ||||
| class GregorianCalendar(CalendarSystem): | ||||
|     """Gregorian calendar system for Calendar.social | ||||
|     """ | ||||
| @@ -104,7 +92,6 @@ class GregorianCalendar(CalendarSystem): | ||||
|         return day_list | ||||
|  | ||||
|     @property | ||||
|     @to_timestamp | ||||
|     def prev_year(self): | ||||
|         """Returns the timestamp of the same date in the previous year | ||||
|         """ | ||||
| @@ -119,7 +106,6 @@ class GregorianCalendar(CalendarSystem): | ||||
|         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 | ||||
|         """ | ||||
| @@ -142,7 +128,6 @@ class GregorianCalendar(CalendarSystem): | ||||
|         return self.month_names[timestamp.month - 1] | ||||
|  | ||||
|     @property | ||||
|     @to_timestamp | ||||
|     def next_month(self): | ||||
|         """Returns the timestamp of the same day in the next month | ||||
|         """ | ||||
| @@ -165,7 +150,6 @@ class GregorianCalendar(CalendarSystem): | ||||
|         return self.month_names[timestamp.month - 1] | ||||
|  | ||||
|     @property | ||||
|     @to_timestamp | ||||
|     def next_year(self): | ||||
|         """Returns the timestamp of the same date in the next year | ||||
|         """ | ||||
| @@ -198,7 +182,7 @@ class GregorianCalendar(CalendarSystem): | ||||
|  | ||||
|         month_end_timestamp = month_start_timestamp.replace(month=next_month) | ||||
|  | ||||
|         return now >= month_start_timestamp and now < month_end_timestamp | ||||
|         return month_start_timestamp <= now < month_end_timestamp | ||||
|  | ||||
|     @staticmethod | ||||
|     def day_events(date, user=None): | ||||
|   | ||||
							
								
								
									
										18
									
								
								calsocial/config_testing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								calsocial/config_testing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| """Configuration file for the development environment | ||||
| """ | ||||
|  | ||||
| ENV = 'testing' | ||||
| #: If ``True``, registration on the site is enabled. | ||||
| REGISTRATION_ENABLED = True | ||||
| #: The default time zone | ||||
| DEFAULT_TIMEZONE = 'Europe/Budapest' | ||||
|  | ||||
| DEBUG = False | ||||
| TESTING = True | ||||
| SQLALCHEMY_DATABASE_URI = 'sqlite:///' | ||||
| SQLALCHEMY_TRACK_MODIFICATIONS = False | ||||
| SECRET_KEY = 'WeAreTesting' | ||||
| SECURITY_PASSWORD_HASH = 'bcrypt' | ||||
| SECURITY_PASSWORD_SALT = SECRET_KEY | ||||
| SECURITY_REGISTERABLE = False | ||||
| CACHE_TYPE = 'simple' | ||||
| @@ -23,15 +23,16 @@ from flask_babelex import lazy_gettext as _ | ||||
| from flask_security.forms import LoginForm as BaseLoginForm | ||||
| from flask_wtf import FlaskForm | ||||
| import pytz | ||||
| from wtforms import BooleanField, PasswordField, SelectField, StringField | ||||
| from wtforms import BooleanField, PasswordField, SelectField, StringField, RadioField | ||||
| from wtforms.ext.dateutil.fields import DateTimeField | ||||
| from wtforms.validators import DataRequired, Email, StopValidation, ValidationError | ||||
| from wtforms.widgets import TextArea | ||||
|  | ||||
| from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS | ||||
| from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, GroupVisibility, \ | ||||
|     GROUP_VISIBILITY_TRANSLATIONS | ||||
|  | ||||
|  | ||||
| class UsernameAvailable(object):  # pylint: disable=too-few-public-methods | ||||
| class UsernameAvailable:  # pylint: disable=too-few-public-methods | ||||
|     """Checks if a username is available | ||||
|     """ | ||||
|  | ||||
| @@ -62,7 +63,7 @@ class UsernameAvailable(object):  # pylint: disable=too-few-public-methods | ||||
|         raise StopValidation(message) | ||||
|  | ||||
|  | ||||
| class EmailAvailable(object):  # pylint: disable=too-few-public-methods | ||||
| class EmailAvailable:  # pylint: disable=too-few-public-methods | ||||
|     """Checks if an email address is available | ||||
|     """ | ||||
|  | ||||
| @@ -394,11 +395,60 @@ class ProfileForm(FlaskForm): | ||||
|     """ | ||||
|  | ||||
|     display_name = StringField(label=_('Display name'), validators=[DataRequired()]) | ||||
|     builtin_avatar = RadioField(label=_('Use a built-in avatar')) | ||||
|     locked = BooleanField(label=_('Lock profile')) | ||||
|  | ||||
|     def __init__(self, profile, *args, **kwargs): | ||||
|         kwargs.update({'display_name': profile.display_name}) | ||||
|         kwargs.update({'locked': profile.locked}) | ||||
|         from flask import current_app | ||||
|  | ||||
|         kwargs.update( | ||||
|             { | ||||
|                 'display_name': profile.display_name, | ||||
|                 'locked': profile.locked, | ||||
|                 'builtin_avatar': profile.builtin_avatar, | ||||
|             }) | ||||
|         FlaskForm.__init__(self, *args, **kwargs) | ||||
|  | ||||
|         self.builtin_avatar.choices = [(name, name) | ||||
|                                        for name in current_app.config['BUILTIN_AVATARS']] | ||||
|         self.profile = profile | ||||
|  | ||||
|  | ||||
| class HandleAvailable:  # pylint: disable=too-few-public-methods | ||||
|     """Checks if a group handle is available | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, message=None): | ||||
|         self.message = message | ||||
|  | ||||
|     def __call__(self, form, field): | ||||
|         from sqlalchemy.orm.exc import NoResultFound | ||||
|  | ||||
|         from calsocial.models import Group | ||||
|  | ||||
|         # If there is no data, we don’t raise an error; it’s not the task of this validator to | ||||
|         # check the validity of the username | ||||
|         if not field.data: | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             Group.query.filter(Group.handle == field.data).one() | ||||
|         except NoResultFound: | ||||
|             return | ||||
|  | ||||
|         if self.message is None: | ||||
|             message = field.gettext('This group handle is not available') | ||||
|         else: | ||||
|             message = self.message | ||||
|  | ||||
|         field.errors[:] = [] | ||||
|         raise StopValidation(message) | ||||
|  | ||||
|  | ||||
| class GroupForm(FlaskForm): | ||||
|     """Form for editing a group | ||||
|     """ | ||||
|  | ||||
|     handle = StringField(label=_('Handle'), validators=[DataRequired(), HandleAvailable()]) | ||||
|     display_name = StringField(label=_('Display name')) | ||||
|     visibility = EnumField(GroupVisibility, GROUP_VISIBILITY_TRANSLATIONS, label=_('Visibility')) | ||||
|   | ||||
							
								
								
									
										100
									
								
								calsocial/group.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								calsocial/group.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| """Group related endpoints for Calendar.social | ||||
| """ | ||||
|  | ||||
| from flask import Blueprint, abort, redirect, render_template, url_for | ||||
| from flask_security import current_user, login_required | ||||
|  | ||||
| from calsocial.utils import RoutedMixin | ||||
|  | ||||
|  | ||||
| class GroupBlueprint(Blueprint, RoutedMixin): | ||||
|     def __init__(self): | ||||
|         Blueprint.__init__(self, 'group', __name__) | ||||
|  | ||||
|         self.app = None | ||||
|  | ||||
|         RoutedMixin.register_routes(self) | ||||
|  | ||||
|     def init_app(self, app, url_prefix=None): | ||||
|         """Initialise the blueprint, registering it with ``app``. | ||||
|         """ | ||||
|  | ||||
|         self.app = app | ||||
|  | ||||
|         app.register_blueprint(self, url_prefix=url_prefix) | ||||
|  | ||||
|     @staticmethod | ||||
|     @RoutedMixin.route('/') | ||||
|     def list_groups(): | ||||
|         """View to list all public groups known by this instance | ||||
|         """ | ||||
|  | ||||
|         from calsocial.models import db, Group, GroupMembership, GroupVisibility | ||||
|  | ||||
|         groups = Group.query | ||||
|  | ||||
|         if current_user.is_authenticated: | ||||
|             groups = groups.outerjoin(GroupMembership) \ | ||||
|                            .filter(db.or_(Group.visibility == GroupVisibility.public, | ||||
|                                           GroupMembership.profile == current_user.profile)) | ||||
|         else: | ||||
|             groups = groups.filter(Group.visibility == GroupVisibility.public) | ||||
|  | ||||
|         return render_template('group/list.html', groups=groups) | ||||
|  | ||||
|     @staticmethod | ||||
|     @login_required | ||||
|     @RoutedMixin.route('/create', methods=['GET', 'POST']) | ||||
|     def create(): | ||||
|         from datetime import datetime | ||||
|  | ||||
|         from .forms import GroupForm | ||||
|         from .models import db, Group, GroupMemberLevel, GroupMembership | ||||
|  | ||||
|         form = GroupForm() | ||||
|  | ||||
|         if form.validate_on_submit(): | ||||
|             group = Group(created_by=current_user.profile) | ||||
|             form.populate_obj(group) | ||||
|             db.session.add(group) | ||||
|  | ||||
|             member = GroupMembership(group=group, | ||||
|                                      profile=current_user.profile, | ||||
|                                      level=GroupMemberLevel.admin, | ||||
|                                      accepted_at=datetime.utcnow(), | ||||
|                                      accepted_by=current_user.profile) | ||||
|             db.session.add(member) | ||||
|  | ||||
|             db.session.commit() | ||||
|  | ||||
|             return redirect(url_for('group.list')) | ||||
|  | ||||
|         return render_template('group/create.html', form=form) | ||||
|  | ||||
|     @staticmethod | ||||
|     @RoutedMixin.route('/<fqn>') | ||||
|     def display(fqn): | ||||
|         from .models import Group | ||||
|  | ||||
|         group = Group.get_by_fqn(fqn) | ||||
|  | ||||
|         if not group.visible_to(current_user.profile): | ||||
|             abort(404) | ||||
|  | ||||
|         return render_template('group/display.html', group=group) | ||||
| @@ -21,12 +21,14 @@ from datetime import datetime | ||||
| from enum import Enum | ||||
| from warnings import warn | ||||
|  | ||||
| from flask import current_app | ||||
| from flask_babelex import lazy_gettext | ||||
| from flask_security import UserMixin, RoleMixin | ||||
| from flask_sqlalchemy import SQLAlchemy | ||||
| from sqlalchemy.orm.exc import NoResultFound | ||||
| from sqlalchemy_utils.types.choice import ChoiceType | ||||
|  | ||||
| from .app_state import app_state_base | ||||
| from .cache import cache | ||||
| from .utils import force_locale | ||||
|  | ||||
| @@ -99,7 +101,7 @@ class ResponseType(Enum): | ||||
|             return self.name.lower() == other.lower()  # pylint: disable=no-member | ||||
|  | ||||
|         if isinstance(other, (int, float)): | ||||
|             return self.value == other | ||||
|             return self.value == other  # pylint: disable=comparison-with-callable | ||||
|  | ||||
|         return Enum.__eq__(self, other) | ||||
|  | ||||
| @@ -121,6 +123,64 @@ EVENT_VISIBILITY_TRANSLATIONS = { | ||||
| } | ||||
|  | ||||
|  | ||||
| class GroupVisibility(Enum): | ||||
|     """Enumeration for group visibility | ||||
|     """ | ||||
|  | ||||
|     #: The group is secret, ie. completely unlisted | ||||
|     secret = 0 | ||||
|  | ||||
|     #: The group is closed, ie. it can be joined only with an invitation, but otherwise public | ||||
|     closed = 1 | ||||
|  | ||||
|     #: The group is public | ||||
|     public = 2 | ||||
|  | ||||
|     def __hash__(self): | ||||
|         return Enum.__hash__(self) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         if isinstance(other, str): | ||||
|             return self.name.lower() == other.lower()  # pylint: disable=no-member | ||||
|  | ||||
|         if isinstance(other, (int, float)): | ||||
|             return self.value == other  # pylint: disable=comparison-with-callable | ||||
|  | ||||
|         return Enum.__eq__(self, other) | ||||
|  | ||||
|  | ||||
| GROUP_VISIBILITY_TRANSLATIONS = { | ||||
|     GroupVisibility.secret: _('Secret'), | ||||
|     GroupVisibility.closed: _('Closed'), | ||||
|     GroupVisibility.public: _('Public'), | ||||
| } | ||||
|  | ||||
|  | ||||
| class GroupMemberLevel(Enum): | ||||
|     """Enumeration for group membership level | ||||
|     """ | ||||
|  | ||||
|     #: The member is a spectator only (ie. read only) | ||||
|     spectator = 0 | ||||
|  | ||||
|     #: The member is a user with all privileges | ||||
|     user = 1 | ||||
|  | ||||
|     #: The member is a moderator | ||||
|     moderator = 2 | ||||
|  | ||||
|     #: The member is an administrator | ||||
|     admin = 3 | ||||
|  | ||||
|  | ||||
| GROUP_MEMBER_LEVEL_TRANSLATIONS = { | ||||
|     GroupMemberLevel.spectator: _('Spectator'), | ||||
|     GroupMemberLevel.user: _('User'), | ||||
|     GroupMemberLevel.moderator: _('Moderator'), | ||||
|     GroupMemberLevel.admin: _('Administrator'), | ||||
| } | ||||
|  | ||||
|  | ||||
| class SettingsProxy: | ||||
|     """Proxy object to get settings for a user | ||||
|     """ | ||||
| @@ -206,7 +266,6 @@ class User(db.Model, UserMixin): | ||||
|         If the user didn’t set a time zone yet, the application default is used. | ||||
|         """ | ||||
|  | ||||
|         from flask import current_app | ||||
|         from pytz import timezone | ||||
|         from pytz.exceptions import UnknownTimeZoneError | ||||
|  | ||||
| @@ -285,6 +344,9 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | ||||
|     #: If locked, a profile cannot be followed without the owner’s consent | ||||
|     locked = db.Column(db.Boolean(), default=False) | ||||
|  | ||||
|     #: If set, the profile will display this builtin avatar | ||||
|     builtin_avatar = db.Column(db.String(length=40), nullable=True) | ||||
|  | ||||
|     @property | ||||
|     def fqn(self): | ||||
|         """The fully qualified name of the profile | ||||
| @@ -786,3 +848,211 @@ class Response(db.Model):  # pylint: disable=too-few-public-methods | ||||
|  | ||||
|     #: The response itself | ||||
|     response = db.Column(db.Enum(ResponseType), nullable=False) | ||||
|  | ||||
|  | ||||
| class AppState(app_state_base(db.Model)):  # pylint: disable=too-few-public-methods,inherit-non-class | ||||
|     """Database model for application state values | ||||
|     """ | ||||
|  | ||||
|     __tablename__ = 'app_state' | ||||
|  | ||||
|     #: The environment that set this key | ||||
|     env = db.Column(db.String(length=40), nullable=False, primary_key=True) | ||||
|  | ||||
|     #: The key | ||||
|     key = db.Column(db.String(length=80), nullable=False, primary_key=True) | ||||
|  | ||||
|     #: The value of the key | ||||
|     value = db.Column(db.Unicode(length=200), nullable=True) | ||||
|  | ||||
|     @classmethod | ||||
|     def __get_state__(cls, key): | ||||
|         try: | ||||
|             record = cls.query \ | ||||
|                         .filter(cls.env == current_app.env) \ | ||||
|                         .filter(cls.key == key) \ | ||||
|                         .one() | ||||
|         except NoResultFound: | ||||
|             return None | ||||
|  | ||||
|         return record.value | ||||
|  | ||||
|     @classmethod | ||||
|     def __set_state__(cls, key, value): | ||||
|         try: | ||||
|             record = cls.query \ | ||||
|                         .filter(cls.env == current_app.env) \ | ||||
|                         .filter(cls.key == key) \ | ||||
|                         .one() | ||||
|         except NoResultFound: | ||||
|             record = cls(env=current_app.env, key=key) | ||||
|  | ||||
|         record.value = value | ||||
|         db.session.add(record) | ||||
|         db.session.commit() | ||||
|  | ||||
|     @classmethod | ||||
|     def __set_state_default__(cls, key, value): | ||||
|         try: | ||||
|             record = cls.query \ | ||||
|                         .filter(cls.env == current_app.env) \ | ||||
|                         .filter(cls.key == key) \ | ||||
|                         .one() | ||||
|         except NoResultFound: | ||||
|             pass | ||||
|         else: | ||||
|             return | ||||
|  | ||||
|         record = cls(env=current_app.env, key=key, value=value) | ||||
|         db.session.add(record) | ||||
|         db.session.commit() | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return f'<AppState {self.env}:{self.key}="{self.value}"' | ||||
|  | ||||
|  | ||||
| class Group(db.Model): | ||||
|     """Database model for groups | ||||
|     """ | ||||
|  | ||||
|     __tablename__ = 'groups' | ||||
|     __table_args__ = ( | ||||
|         db.UniqueConstraint('handle', 'domain'), | ||||
|     ) | ||||
|  | ||||
|     id = db.Column(db.Integer(), primary_key=True) | ||||
|  | ||||
|     handle = db.Column(db.String(length=50), nullable=False) | ||||
|  | ||||
|     domain = db.Column(db.String(length=100), nullable=True) | ||||
|  | ||||
|     display_name = db.Column(db.Unicode(length=100), nullable=True) | ||||
|  | ||||
|     created_at = db.Column(db.DateTime(), default=datetime.utcnow) | ||||
|  | ||||
|     created_by_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), nullable=False) | ||||
|  | ||||
|     created_by = db.relationship('Profile') | ||||
|  | ||||
|     default_level = db.Column(db.Enum(GroupMemberLevel)) | ||||
|  | ||||
|     visibility = db.Column(db.Enum(GroupVisibility), nullable=False) | ||||
|  | ||||
|     @property | ||||
|     def members(self): | ||||
|         return Profile.query.join(GroupMembership, GroupMembership.profile_id == Profile.id).filter(GroupMembership.group == self) | ||||
|  | ||||
|     @property | ||||
|     def fqn(self): | ||||
|         """The fully qualified name of the group | ||||
|  | ||||
|         For local profiles, this is in the form ``!username``; for remote users, it’s in the form | ||||
|         ``!handle@domain``. | ||||
|         """ | ||||
|  | ||||
|         if not self.domain: | ||||
|             return f'!{self.handle}' | ||||
|  | ||||
|         return f'!{self.handle}@{self.domain}' | ||||
|  | ||||
|     @property | ||||
|     def url(self): | ||||
|         """Get the URL for this group | ||||
|         """ | ||||
|  | ||||
|         from flask import url_for | ||||
|  | ||||
|         return url_for('group.display', fqn=self.fqn) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return f'<Group {self.id}: !{self.handle}@{self.domain}>' | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.fqn | ||||
|  | ||||
|     @classmethod | ||||
|     def get_by_fqn(cls, fqn): | ||||
|         import re | ||||
|  | ||||
|         match = re.match(r'^!([a-z0-9_]+)(@.*)?', fqn) | ||||
|  | ||||
|         if not match: | ||||
|             raise ValueError(f'Invalid Group FQN {fqn}') | ||||
|  | ||||
|         handle, domain = match.groups() | ||||
|  | ||||
|         return Group.query.filter(Group.handle == handle).filter(Group.domain == domain).one() | ||||
|  | ||||
|     def visible_to(self, profile): | ||||
|         """Checks whether this group is visible to ``profile`` | ||||
|  | ||||
|         It is so if the group is public or closed or, given it is secret, ``profile`` is a member | ||||
|         of the group. | ||||
|  | ||||
|         :param profile: a :class:`Profile` object, or ``None`` to check for anonymous access | ||||
|         :type profile: Profile | ||||
|         :returns: ``True`` if the group is visible, ``False`` otherwise | ||||
|         :rtype: bool | ||||
|         """ | ||||
|  | ||||
|         if self.visibility == GroupVisibility.secret: | ||||
|             try: | ||||
|                 GroupMembership.query \ | ||||
|                                .filter(GroupMembership.group == self) \ | ||||
|                                .filter(GroupMembership.profile == profile) \ | ||||
|                                .one() | ||||
|             except NoResultFound: | ||||
|                 return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def details_visible_to(self, profile): | ||||
|         """Checks whether the details of this group is visible to ``profile`` | ||||
|  | ||||
|         Details include member list and events shared with this group. | ||||
|  | ||||
|         It is so if the group is public or ``profile`` is a member of the group. | ||||
|  | ||||
|         :param profile: a :class:`Profile` object, or ``None`` to check for anonymous access | ||||
|         :type profile: Profile | ||||
|         :returns: ``True`` if the group is visible, ``False`` otherwise | ||||
|         :rtype: bool | ||||
|         """ | ||||
|  | ||||
|         if self.visibility != GroupVisibility.public: | ||||
|             try: | ||||
|                 GroupMembership.query \ | ||||
|                                .filter(GroupMembership.group == self) \ | ||||
|                                .filter(GroupMembership.profile == profile) \ | ||||
|                                .one() | ||||
|             except NoResultFound: | ||||
|                 return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|  | ||||
| class GroupMembership(db.Model): | ||||
|     """Database model for group membership | ||||
|     """ | ||||
|  | ||||
|     __tablename__ = 'group_members' | ||||
|  | ||||
|     group_id = db.Column(db.Integer(), db.ForeignKey('groups.id'), primary_key=True) | ||||
|  | ||||
|     group = db.relationship('Group') | ||||
|  | ||||
|     profile_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), primary_key=True) | ||||
|  | ||||
|     profile = db.relationship('Profile', foreign_keys=[profile_id]) | ||||
|  | ||||
|     level = db.Column(db.Enum(GroupMemberLevel), nullable=False) | ||||
|  | ||||
|     requested_at = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False) | ||||
|  | ||||
|     joined_at = db.Column(db.DateTime(), nullable=True) | ||||
|  | ||||
|     accepted_at = db.Column(db.DateTime(), nullable=True) | ||||
|  | ||||
|     accepted_by_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), nullable=True) | ||||
|  | ||||
|     accepted_by = db.relationship('Profile', foreign_keys=[accepted_by_id]) | ||||
|   | ||||
| @@ -35,6 +35,15 @@ class AnonymousUser(BaseAnonymousUser): | ||||
|  | ||||
|         return current_app.timezone | ||||
|  | ||||
|     @property | ||||
|     def profile(self): | ||||
|         """The profile of the anonymous user | ||||
|  | ||||
|         Always evaluates to ``None`` | ||||
|         """ | ||||
|  | ||||
|         return None | ||||
|  | ||||
|  | ||||
| @user_logged_in.connect | ||||
| def login_handler(app, user):  # pylint: disable=unused-argument | ||||
|   | ||||
							
								
								
									
										35
									
								
								calsocial/static/avatars/doctor.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								calsocial/static/avatars/doctor.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  --> | ||||
| <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User2_doctor.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid2331" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview> | ||||
| <g id="g52" transform="matrix(223.2 0 0 228.51 -1.9511e6 -1.9794e6)"> | ||||
| 		<radialGradient id="XMLID_82_" gradientUnits="userSpaceOnUse" cx="8790" cy="8685.3" r="36.346"> | ||||
| 			<stop id="stop55" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop57" style="stop-color:#000000" offset="1"/> | ||||
| 		</radialGradient> | ||||
| 		<circle id="circle59" sodipodi:rx="17.433001" sodipodi:ry="17.433001" style="fill:url(#XMLID_82_)" cx="8782.5" cy="8679.2" sodipodi:cy="8679.21" sodipodi:cx="8782.4932" r="17.433"/> | ||||
| 		<linearGradient id="XMLID_83_" y2="8706.5" gradientUnits="userSpaceOnUse" y1="8762" x2="8747.4" x1="8818.9"> | ||||
| 			<stop id="stop62" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop64" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<path id="path66" style="fill:url(#XMLID_83_)" d="m8782.8 8697.6c-15.7 0-28.7 23.1-31 53.3h61.9c-2.2-30.2-15.2-53.3-30.9-53.3z"/> | ||||
| 		<path id="path68" style="fill:#c6c7c8" d="m8768.3 8669c-1 1.3-1.8 2.8-2.3 4.4h33c-0.6-1.6-1.3-3.1-2.3-4.4h-28.4z"/> | ||||
| 		<circle id="circle70" sodipodi:rx="6.0469999" sodipodi:ry="6.0469999" style="fill:#ffffff" cx="8782.5" cy="8671.2" sodipodi:cy="8671.1943" sodipodi:cx="8782.4932" r="6.047"/> | ||||
| 		<circle id="circle72" sodipodi:rx="1.501" sodipodi:ry="1.501" style="fill:#c6c7c8" cx="8782.5" cy="8671.2" sodipodi:cy="8671.1943" sodipodi:cx="8782.4941" r="1.501"/> | ||||
| 		<g id="g74"> | ||||
| 			<circle id="circle76" sodipodi:rx="2.622" sodipodi:ry="2.622" style="stroke:#c6c7c8;stroke-width:.85040;fill:#ffffff" cx="8791" cy="8718.2" sodipodi:cy="8718.166" sodipodi:cx="8790.9648" r="2.622"/> | ||||
| 			<g id="g78"> | ||||
| 				<path id="path80" style="fill:#b3b3b3" d="m8771.9 8714.2c-1.8 0.5-3.2 1.8-4.1 3.5-1 1.8-1.2 4.1-0.5 6.2 0.4 1.5 1.3 2.8 2.5 3.8l0.1 0.1 1.9-0.6-0.5-0.2c-1.2-0.9-2.1-2.1-2.6-3.6-0.5-1.6-0.4-3.4 0.4-4.9 0.7-1.4 1.9-2.4 3.3-2.8 1.4-0.5 2.9-0.3 4.3 0.4 1.5 0.7 2.6 2.1 3.2 3.8 0.4 1.5 0.4 3-0.2 4.4l-0.2 0.5 1.9-0.6v-0.1c0.4-1.5 0.4-3.1-0.1-4.6-1.3-4.2-5.5-6.6-9.4-5.3z"/> | ||||
| 				<path id="path82" style="fill:#b2b2b2" d="m8768.5 8723.5c-1.1-3.4 0.6-7.1 3.8-8.1s6.7 1 7.8 4.4c0.5 1.6 0.4 3.2-0.1 4.6l1.2-0.4c0.4-1.4 0.4-2.9-0.1-4.5-1.3-4-5.4-6.3-9.1-5.1-3.8 1.2-5.8 5.4-4.5 9.4 0.5 1.5 1.4 2.8 2.5 3.8l1.2-0.4c-1.2-0.8-2.2-2.1-2.7-3.7z"/> | ||||
| 				<path id="path84" style="fill:#b3b3b3" d="m8770.1 8726c-0.8 0.3-1.3 1.2-1 2 0.1 0.4 0.4 0.8 0.8 1 0.4 0.1 0.8 0.2 1.1 0.1 0.4-0.2 0.7-0.4 0.9-0.8 0.2-0.3 0.2-0.8 0.1-1.2-0.2-0.4-0.4-0.8-0.8-1s-0.8-0.2-1.1-0.1z"/> | ||||
| 					<ellipse id="ellipse86" sodipodi:rx="1.261" sodipodi:ry="1.3559999" style="fill:#b2b2b2" cx="8770.5" cy="8727.5" rx="1.261" ry="1.356" transform="matrix(.9537 -.3009 .3009 .9537 -2219.4 3043)" sodipodi:cy="8727.5391" sodipodi:cx="8770.5449"/> | ||||
| 				<path id="path88" style="fill:#b3b3b3" d="m8780.2 8722.8c-0.4 0.1-0.7 0.4-0.9 0.7-0.1 0.3-0.1 0.5-0.1 0.8v0.5c0.3 0.8 1.2 1.3 2 1.1 0.7-0.3 1.2-1.2 0.9-2s-1.1-1.3-1.9-1.1z"/> | ||||
| 					<ellipse id="ellipse90" sodipodi:rx="1.261" sodipodi:ry="1.3559999" style="fill:#b2b2b2" cx="8780.7" cy="8724.3" rx="1.261" ry="1.356" transform="matrix(.9536 -.301 .301 .9536 -2219 3047.9)" sodipodi:cy="8724.3447" sodipodi:cx="8780.6729"/> | ||||
| 			</g> | ||||
| 			<path id="path92" style="fill:#b3b3b3" d="m8792.1 8715.9l0.8 0.8c2.2-2.6 3.5-6 3.5-9.6 0-1.3-0.1-2.5-0.4-3.7-0.6-0.5-1.1-1-1.7-1.4 0.7 1.6 1 3.3 1 5.1 0 3.3-1.2 6.4-3.2 8.8z"/> | ||||
| 			<path id="path94" style="fill:#b3b3b3" d="m8771.4 8715.5l1.1-0.4c-1.6-2.3-2.6-5-2.6-8 0-1.7 0.3-3.3 0.9-4.7-0.6 0.4-1.1 0.9-1.6 1.4-0.3 1-0.4 2.1-0.4 3.3 0 3.1 1 6 2.6 8.4z"/> | ||||
| 		</g> | ||||
| 	</g> | ||||
| <g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none"> | ||||
| 	<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/> | ||||
| </g> | ||||
| <metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11056/users-by-sampler-11056</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg> | ||||
| After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										54
									
								
								calsocial/static/avatars/engineer.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								calsocial/static/avatars/engineer.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										91
									
								
								calsocial/static/avatars/scientist.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								calsocial/static/avatars/scientist.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  --> | ||||
| <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User10_scientist.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview> | ||||
| <g id="g616" transform="matrix(220.73 0 0 227.54 -1.9119e6 -1.9962e6)"> | ||||
| 			<linearGradient id="XMLID_112_" y2="8817.8" gradientUnits="userSpaceOnUse" y1="8873.4" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="8667.5" x1="8739.2"> | ||||
| 			<stop id="stop619" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop621" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<path id="path623" style="fill:url(#XMLID_112_)" d="m8703 8808.6c-15.7 0-28.7 23.2-31 53.4l62 0.1c-2.2-30.3-15.2-53.5-31-53.5z"/> | ||||
| 			<radialGradient id="XMLID_113_" gradientUnits="userSpaceOnUse" cy="8796.6" cx="8709.6" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="36.411"> | ||||
| 			<stop id="stop626" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop628" style="stop-color:#000000" offset="1"/> | ||||
| 		</radialGradient> | ||||
| 		<circle id="circle630" sodipodi:rx="17.464001" sodipodi:ry="17.464001" style="fill:url(#XMLID_113_)" cx="8702.1" cy="8790.2" sodipodi:cy="8790.2412" sodipodi:cx="8702.1367" r="17.464"/> | ||||
| 		<g id="g632"> | ||||
| 			<path id="path634" style="fill:#c6c7c8" d="m8727.6 8800.3c-1.3 0-2.4 1.1-2.4 2.4 0 0.5 0 1.4 0.4 2.2 0.1 0.3 0.3 0.6 0.5 0.8 0.2 0.1 0.3 0.3 0.5 0.4v3.1c-0.5 1-5.8 10.7-5.8 10.7v-0.1c-0.6 1-1 2.7-0.1 4.1 0.8 1.5 2.4 2.2 4.9 2.2h11.3c2.4 0 4.1-0.7 4.9-2.2 0.8-1.4 0.4-3.1-0.2-4.1v0.1s-5.2-9.7-5.8-10.7v-3.1c0.2-0.1 0.4-0.3 0.5-0.4 0.3-0.2 0.4-0.5 0.6-0.8 0.3-0.8 0.3-1.7 0.3-2.2 0-1.3-1-2.4-2.4-2.4h-7.2zm3.5 10.7c0.1-0.1 0.1-0.2 0.1-0.2s0.1 0.1 0.1 0.2c0 0 4.8 8.8 5.6 10.3h-11.3-0.1c0.8-1.5 5.6-10.3 5.6-10.3zm-6.1 11.2v0.1-0.1z"/> | ||||
| 			<path id="path636" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:#ffffff" d="m8736.9 8823.7c4.3 0 2.6-2.6 2.6-2.6l-6.1-11.3v-4.7-0.5c0.1-0.6 0.9-0.3 1.3-0.6 0.1-0.5 0.1-1.3 0.1-1.3h-7.2s0 0.8 0.2 1.3c0.3 0.3 1.1 0 1.2 0.6v5.2l-6.1 11.3s-1.6 2.6 2.7 2.6h11.3z"/> | ||||
| 			<path id="path638" style="fill:#ffffff" enable-background="new    " d="m8733 8810.1c0-0.1-0.1-0.2-0.1-0.3v-5.2c0.1-0.7 0.8-0.8 1.1-0.9 0.1 0 0.2 0 0.2-0.1 0.1-0.1 0.1-0.3 0.1-0.4h-6.2c0 0.1 0 0.3 0.1 0.4 0 0.1 0.2 0.1 0.2 0.1 0.4 0.1 1 0.2 1.1 0.9v5.2c0 0.1 0 0.2-0.1 0.3 0 0-2 3.7-3.7 6.8h11l-3.7-6.8z"/> | ||||
| 			<radialGradient id="XMLID_114_" gradientUnits="userSpaceOnUse" cx="8731.2" cy="8820.1" r="6.1747"> | ||||
| 				<stop id="stop641" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 				<stop id="stop643" style="stop-color:#C6C7C8" offset="1"/> | ||||
| 			</radialGradient> | ||||
| 			<path id="path645" style="fill:url(#XMLID_114_)" d="m8736.7 8816.9h-11c-1.3 2.3-2.4 4.4-2.4 4.4s-0.2 0.4-0.2 0.8c0 0.1 0 0.3 0.1 0.4 0.2 0.5 1.1 0.7 2.4 0.7h11.3c1.2 0 2.1-0.2 2.4-0.7 0-0.1 0.1-0.3 0.1-0.4 0-0.4-0.3-0.8-0.3-0.8l-2.4-4.4z"/> | ||||
| 			<g id="g647"> | ||||
| 					<line id="line649" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8729.1" y1="8815.7" x2="8726.1" y2="8815.7"/> | ||||
| 					<line id="line651" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8727.4" y1="8818.4" x2="8724.4" y2="8818.4"/> | ||||
| 					<line id="line653" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8730.7" y1="8812.6" x2="8727.6" y2="8812.6"/> | ||||
| 					<line id="line655" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8725.9" y1="8821.4" x2="8722.9" y2="8821.4"/> | ||||
| 			</g> | ||||
| 			<radialGradient id="XMLID_115_" gradientUnits="userSpaceOnUse" cx="8731" cy="8799.4" r="2.1055"> | ||||
| 				<stop id="stop658" style="stop-color:#F0F3E4" offset=".0112"/> | ||||
| 				<stop id="stop660" style="stop-color:#C6C7C8" offset=".4946"/> | ||||
| 				<stop id="stop662" style="stop-color:#C6C7C8" offset=".9964"/> | ||||
| 			</radialGradient> | ||||
| 			<circle id="circle664" sodipodi:rx="2.1059999" sodipodi:ry="2.1059999" style="fill:url(#XMLID_115_)" cx="8731" cy="8799.4" sodipodi:cy="8799.4082" sodipodi:cx="8731.0234" r="2.106"/> | ||||
| 			<radialGradient id="XMLID_116_" gradientUnits="userSpaceOnUse" cx="8731.5" cy="8797.4" r="1.2222"> | ||||
| 				<stop id="stop667" style="stop-color:#F0F3E4" offset=".0112"/> | ||||
| 				<stop id="stop669" style="stop-color:#C6C7C8" offset=".4982"/> | ||||
| 				<stop id="stop671" style="stop-color:#C9CACB" offset="1"/> | ||||
| 			</radialGradient> | ||||
| 			<circle id="circle673" sodipodi:rx="1.222" sodipodi:ry="1.222" style="fill:url(#XMLID_116_)" cx="8731.5" cy="8797.4" sodipodi:cy="8797.4023" sodipodi:cx="8731.5215" r="1.222"/> | ||||
| 			<radialGradient id="XMLID_117_" gradientUnits="userSpaceOnUse" cx="8730.2" cy="8794.7" r=".65530"> | ||||
| 				<stop id="stop676" style="stop-color:#F0F3E4" offset=".0112"/> | ||||
| 				<stop id="stop678" style="stop-color:#C6C7C8" offset=".4729"/> | ||||
| 				<stop id="stop680" style="stop-color:#C6C7C8" offset="1"/> | ||||
| 			</radialGradient> | ||||
| 			<circle id="circle682" sodipodi:rx="0.65499997" sodipodi:ry="0.65499997" style="fill:url(#XMLID_117_)" cx="8730.2" cy="8794.7" sodipodi:cy="8794.7402" sodipodi:cx="8730.1738" r="0.655"/> | ||||
| 		</g> | ||||
| 		<linearGradient id="XMLID_118_" y2="8822.4" gradientUnits="userSpaceOnUse" y1="8836.1" x2="8725.3" x1="8742.9"> | ||||
| 			<stop id="stop685" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop687" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle689" sodipodi:rx="7.96" sodipodi:ry="7.96" style="fill:url(#XMLID_118_)" cx="8734.5" cy="8829.6" sodipodi:cy="8829.5977" sodipodi:cx="8734.5234" r="7.96"/> | ||||
| 		<linearGradient id="XMLID_119_" y2="8829.8" gradientUnits="userSpaceOnUse" y1="8832.6" x2="8721.9" x1="8725.5"> | ||||
| 			<stop id="stop692" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop694" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle696" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_119_)" cx="8723.8" cy="8831.2" sodipodi:cy="8831.2256" sodipodi:cx="8723.7988" r="1.628"/> | ||||
| 		<linearGradient id="XMLID_120_" y2="8824" gradientUnits="userSpaceOnUse" y1="8826.8" x2="8722.5" x1="8726.1"> | ||||
| 			<stop id="stop699" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop701" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle703" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_120_)" cx="8724.4" cy="8825.5" sodipodi:cy="8825.4512" sodipodi:cx="8724.374" r="1.628"/> | ||||
| 		<linearGradient id="XMLID_121_" y2="8819.7" gradientUnits="userSpaceOnUse" y1="8822.5" x2="8726.5" x1="8730.1"> | ||||
| 			<stop id="stop706" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop708" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle710" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_121_)" cx="8728.4" cy="8821.1" sodipodi:cy="8821.1387" sodipodi:cx="8728.3525" r="1.628"/> | ||||
| 		<linearGradient id="XMLID_122_" y2="8817.1" gradientUnits="userSpaceOnUse" y1="8819.9" x2="8731.3" x1="8734.9"> | ||||
| 			<stop id="stop713" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop715" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle717" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_122_)" cx="8733.2" cy="8818.6" sodipodi:cy="8818.6035" sodipodi:cx="8733.1895" r="1.628"/> | ||||
| 		<linearGradient id="XMLID_123_" y2="8817" gradientUnits="userSpaceOnUse" y1="8821.1" x2="8737.2" x1="8742.5"> | ||||
| 			<stop id="stop720" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop722" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<circle id="circle724" sodipodi:rx="2.385" sodipodi:ry="2.385" style="fill:url(#XMLID_123_)" cx="8740" cy="8819.1" sodipodi:cy="8819.1309" sodipodi:cx="8740.0049" r="2.385"/> | ||||
| 		<polygon id="polygon726" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" points="8687.8 8828.7 8687.8 8834 8693.3 8836.9 8699.1 8834 8699.1 8828.7"/> | ||||
| 		<rect id="rect728" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="2.886" width="11.239" y="8825.8" x="8687.8"/> | ||||
| 		<rect id="rect730" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="5.163" width="0.988" y="8820.5" x="8689.4"/> | ||||
| 		<rect id="rect732" style="fill:#ffffff" height="0.607" width="1.063" y="8820.5" x="8689.4"/> | ||||
| 		<rect id="rect734" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="5.163" width="0.987" y="8820.5" x="8691.3"/> | ||||
| 		<rect id="rect736" style="stroke:#ffffff;stroke-width:.2278;fill:#ffffff" height="0.607" width="1.063" y="8821" x="8691.3"/> | ||||
| 		<polyline id="polyline738" style="stroke:#ffffff;stroke-width:.2278;stroke-linecap:round;fill:none" points="8692.4 8821.3 8692.8 8821.7 8692.8 8824.2"/> | ||||
| 			<line id="line740" style="stroke:#ffffff;stroke-width:.2278;stroke-linecap:round;fill:none" x1="8691.8" y1="8822.9" x2="8691.8" y2="8825.4"/> | ||||
| 	</g> | ||||
| <g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none"> | ||||
| 	<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/> | ||||
| </g> | ||||
| <metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11064/users-by-sampler-11064</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg> | ||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										39
									
								
								calsocial/static/avatars/statistician.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								calsocial/static/avatars/statistician.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  --> | ||||
| <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User11_businessman.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="24.827256" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview> | ||||
| <g id="g1012" transform="matrix(202.56 0 0 211.14 -1.7757e6 -1.8519e6)"> | ||||
| 		<line id="line1014" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8793.2" x2="8853.8" y2="8793.2"/> | ||||
| 		<line id="line1016" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8809.3" x2="8853.8" y2="8809.3"/> | ||||
| 		<line id="line1018" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8825.4" x2="8853.8" y2="8825.4"/> | ||||
| 		<line id="line1020" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8841.5" x2="8853.8" y2="8841.5"/> | ||||
| 		<line id="line1022" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8857.6" x2="8853.8" y2="8857.6"/> | ||||
| 		<line id="line1024" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8777.1" x2="8853.8" y2="8777.1"/> | ||||
| 			<linearGradient id="XMLID_129_" y2="8818.6" gradientUnits="userSpaceOnUse" y1="8872.7" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="8771.8" x1="8841.5"> | ||||
| 			<stop id="stop1027" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop1029" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<path id="path1031" style="fill:url(#XMLID_129_)" d="m8806.3 8809.7c-15.3 0-27.9 22.6-30.2 52h60.4c-2.2-29.4-14.9-52-30.2-52z"/> | ||||
| 			<radialGradient id="XMLID_130_" gradientUnits="userSpaceOnUse" cy="8797.9" cx="8812.7" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="35.435"> | ||||
| 			<stop id="stop1034" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop1036" style="stop-color:#000000" offset="1"/> | ||||
| 		</radialGradient> | ||||
| 		<circle id="circle1038" sodipodi:rx="16.996" sodipodi:ry="16.996" style="fill:url(#XMLID_130_)" cx="8805.4" cy="8791.8" sodipodi:cy="8791.8291" sodipodi:cx="8805.4414" r="16.996"/> | ||||
| 		<g id="g1040"> | ||||
| 			<polyline id="polyline1042" style="stroke-linejoin:round;stroke:#c6c7c8;stroke-width:3.1577;stroke-linecap:round;fill:none" points="8770.2 8837.3 8776.3 8809.9 8786.4 8867.7 8796.5 8830.1 8808.4 8848.4 8822.4 8812.2 8827.5 8833.4 8849.5 8777.1"/> | ||||
| 			<circle id="circle1044" sodipodi:rx="4.283" sodipodi:ry="4.283" style="fill:#c6c7c8" cx="8849.5" cy="8777.1" sodipodi:cy="8777.0605" sodipodi:cx="8849.5098" r="4.283"/> | ||||
| 		</g> | ||||
| 		<g id="g1046"> | ||||
| 			<path id="path1048" style="fill:#333333" d="m8768.5 8787v-4.4h0.8l1.1 3.1c0.1 0.3 0.1 0.5 0.2 0.6 0-0.1 0.1-0.4 0.2-0.7l1.1-3h0.7v4.4h-0.5v-3.7l-1.3 3.7h-0.5l-1.3-3.7v3.7h-0.5z"/> | ||||
| 			<path id="path1050" style="fill:#333333" d="m8773.7 8787l1.7-4.4h0.6l1.8 4.4h-0.7l-0.5-1.4h-1.8l-0.5 1.4h-0.6zm1.2-1.8h1.5l-0.4-1.2c-0.2-0.4-0.3-0.7-0.3-0.9-0.1 0.2-0.2 0.5-0.3 0.8l-0.5 1.3z"/> | ||||
| 			<path id="path1052" style="fill:#333333" d="m8778.4 8787l1.7-2.3-1.5-2.1h0.7l0.8 1.1c0.1 0.3 0.3 0.4 0.3 0.6 0.1-0.2 0.2-0.4 0.4-0.5l0.8-1.2h0.7l-1.5 2.1 1.6 2.3h-0.7l-1.1-1.6c-0.1-0.1-0.1-0.2-0.2-0.3-0.1 0.2-0.2 0.3-0.2 0.4l-1.1 1.5h-0.7z"/> | ||||
| 		</g> | ||||
| 		<g id="g1054"> | ||||
| 			<path id="path1056" style="fill:#333333" d="m8841.3 8854v-4.4h0.9l1 3.1c0.1 0.3 0.2 0.5 0.2 0.7 0.1-0.2 0.1-0.4 0.3-0.7l1-3.1h0.8v4.4h-0.6v-3.7l-1.3 3.7h-0.5l-1.2-3.7v3.7h-0.6z"/> | ||||
| 			<path id="path1058" style="fill:#333333" d="m8847.1 8854v-4.4h0.6v4.4h-0.6z"/> | ||||
| 			<path id="path1060" style="fill:#333333" d="m8849.3 8854v-4.4h0.6l2.3 3.4v-3.4h0.5v4.4h-0.6l-2.2-3.4v3.4h-0.6z"/> | ||||
| 		</g> | ||||
| 	</g> | ||||
| <g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none"> | ||||
| 	<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/> | ||||
| </g> | ||||
| <metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11065/users-by-sampler-11065</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg> | ||||
| After Width: | Height: | Size: 5.9 KiB | 
							
								
								
									
										11
									
								
								calsocial/static/avatars/user.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								calsocial/static/avatars/user.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  --> | ||||
| <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2" sodipodi:docname="sampler_User1_in_suit.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><defs id="defs1434"> | ||||
| <radialGradient id="radialGradient4660" gradientUnits="userSpaceOnUse" cy="8685.3" cx="8710.2" r="36.396" inkscape:collect="always"><stop id="stop35" style="stop-color:#FFFFFF" offset="0"/><stop id="stop37" style="stop-color:#000000" offset="1"/></radialGradient><linearGradient id="linearGradient4662" y2="8706.6" gradientUnits="userSpaceOnUse" x2="8667.6" y1="8762.1" x1="8739.2" inkscape:collect="always"><stop id="stop42" style="stop-color:#FFFFFF" offset="0"/><stop id="stop44" style="stop-color:#000000" offset="1"/></linearGradient></defs><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="5.33" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="g4648" inkscape:cx="50" inkscape:cy="46.515666" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid2323" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview> | ||||
| <g id="g32" transform="matrix(28.594 0 0 28.594 -2.557e5 -2.4244e5)"> | ||||
| 		<g id="g4648"><g id="g4654" transform="matrix(7.5835 0 0 8.0832 -56740 -61545)"><circle id="circle39" sodipodi:rx="17.457001" sodipodi:ry="17.457001" style="fill:url(#radialGradient4660)" cx="8702.7" cy="8679.2" sodipodi:cy="8679.2344" sodipodi:cx="8702.7109" r="17.457"/><path id="path46" style="fill:url(#linearGradient4662)" d="m8703 8697.6c-15.7 0-28.7 23.2-31 53.4h62c-2.3-30.2-15.3-53.4-31-53.4z"/><polygon id="polygon48" style="fill:#c6c7c8" points="8700.2 8708 8697.4 8703.1 8703 8698.3 8703 8698.3 8708.6 8703.1 8705.8 8708"/><path id="path50" style="fill:#c6c7c8" d="m8695.4 8737.1l7.6 10.3v-38.7h-2.7l-4.9 28.4zm10.4-28.5h-2.7v38.8l7.6-10.3-4.9-28.5z"/></g></g> | ||||
| 	</g> | ||||
| <g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none"> | ||||
| 	<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/> | ||||
| </g> | ||||
| <metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11055/users-by-sampler-11055</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg> | ||||
| After Width: | Height: | Size: 4.0 KiB | 
							
								
								
									
										26
									
								
								calsocial/static/avatars/whoami.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								calsocial/static/avatars/whoami.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  --> | ||||
| <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2" sodipodi:docname="sampler_User9_no_idea.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview> | ||||
| <g id="g522" transform="matrix(219.98 0 0 229.49 -2.061e6 -1.9878e6)"> | ||||
| 			<linearGradient id="XMLID_108_" y2="8705.9" gradientUnits="userSpaceOnUse" y1="8761.1" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="9375.1" x1="9446.2"> | ||||
| 			<stop id="stop525" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop527" style="stop-color:#000000" offset="1"/> | ||||
| 		</linearGradient> | ||||
| 		<path id="path529" style="fill:url(#XMLID_108_)" d="m9410.5 8697.4c-15.6-0.1-28.5 22.9-30.8 52.9l61.5 0.1c-2.2-30-15.1-53-30.7-53z"/> | ||||
| 			<radialGradient id="XMLID_109_" gradientUnits="userSpaceOnUse" cy="8684.8" cx="9416.8" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="36.129"> | ||||
| 			<stop id="stop532" style="stop-color:#FFFFFF" offset="0"/> | ||||
| 			<stop id="stop534" style="stop-color:#000000" offset="1"/> | ||||
| 		</radialGradient> | ||||
| 		<circle id="circle536" sodipodi:rx="17.329" sodipodi:ry="17.329" style="fill:url(#XMLID_109_)" cx="9409.6" cy="8679.1" sodipodi:cy="8679.1064" sodipodi:cx="9409.5693" r="17.329"/> | ||||
| 		<g id="g538"> | ||||
| 			<g id="g542"> | ||||
| 				<g id="g544"> | ||||
| 					<path id="path546" style="stroke:#8e8f91;stroke-width:.56350;fill:#ffffff" d="m9404.8 8668.8c1.2-0.8 2.7-1.2 4.5-1.2 2.3 0 4.3 0.6 5.8 1.7s2.3 2.7 2.3 4.9c0 1.3-0.3 2.5-1 3.4-0.4 0.5-1.1 1.3-2.2 2.1l-1.1 0.9c-0.6 0.4-1 1-1.2 1.6-0.1 0.4-0.2 1-0.2 1.8h-4.2c0-1.7 0.2-2.9 0.5-3.6 0.2-0.7 0.9-1.4 2-2.3l1.2-0.9 0.9-0.9c0.4-0.5 0.6-1.2 0.6-1.8 0-0.8-0.3-1.5-0.7-2.2-0.5-0.6-1.3-0.9-2.5-0.9s-2.1 0.4-2.6 1.1c-0.5 0.8-0.7 1.7-0.7 2.5h-4.5c0.2-2.9 1.2-5 3.1-6.2zm2.6 17.4h4.6v4.4h-4.6v-4.4z"/> | ||||
| 				</g> | ||||
| 			</g> | ||||
| 		</g> | ||||
| 	</g> | ||||
| <g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none"> | ||||
| 	<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/> | ||||
| </g> | ||||
| <metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11063/users-by-sampler-11063</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg> | ||||
| After Width: | Height: | Size: 4.3 KiB | 
| @@ -18,3 +18,13 @@ | ||||
|     {% endif %} | ||||
| </div> | ||||
| {% endmacro %} | ||||
|  | ||||
| {% macro profile_link(profile) %} | ||||
| <a href="{% if profile %}{{ url_for('display_profile', username=profile.user.username) }}{% else %}#{% endif %}" class="ui profile"> | ||||
|     {% if profile and profile.builtin_avatar %} | ||||
|     <img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular avatar image"> | ||||
|     {% endif %} | ||||
|     <div class="display name">{{ profile.display_name }}</div> | ||||
|     <div class="handle">{{ profile or '' }}</div> | ||||
| </a> | ||||
| {% endmacro %} | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
|     <br> | ||||
|     {% endif %} | ||||
|  | ||||
|     {{ field(form.builtin_avatar) }} | ||||
|     {{ field(form.display_name) }} | ||||
|     {{ field(form.locked, inline=true) }} | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,7 @@ | ||||
|     {%- endtrans %} | ||||
|                     </div> | ||||
|                     <a class="item" href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a> | ||||
|                     <a class="item" href="{{ url_for('group.list_groups') }}">{% trans %}Groups{% endtrans %}</a> | ||||
|                     <a class="item" href="{{ url_for('account.notifications') }}">{% trans %}Notifications{% endtrans %}</a> | ||||
|                     <a class="item" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a> | ||||
|                     <a class="item" href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a> | ||||
|   | ||||
							
								
								
									
										17
									
								
								calsocial/templates/group/create.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								calsocial/templates/group/create.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% from '_macros.html' import field %} | ||||
|  | ||||
| {% block content %} | ||||
| <h2>{% trans %}Create a group{% endtrans %}</h2> | ||||
|  | ||||
| <form method="post" class="ui form"> | ||||
|     {{ form.hidden_tag() }} | ||||
|  | ||||
|     {{ field(form.handle) }} | ||||
|     {{ field(form.display_name) }} | ||||
|     {{ field(form.visibility) }} | ||||
|  | ||||
|     <button class="ui primary button">{% trans %}Create{% endtrans %}</button> | ||||
|     <a href="{{ url_for('group.list_groups') }}" class="ui button">{% trans %}Cancel{% endtrans %}</a> | ||||
| </form> | ||||
| {% endblock content %} | ||||
							
								
								
									
										33
									
								
								calsocial/templates/group/display.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								calsocial/templates/group/display.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% from '_macros.html' import profile_link %} | ||||
|  | ||||
| {% block content %} | ||||
| <h2 class="ui header"> | ||||
|     {% if group.visibility == 'secret' %} | ||||
|     [secret] | ||||
|     {% elif group.visibility == 'closed' %} | ||||
|     [closed] | ||||
|     {% elif group.visibility == 'public' %} | ||||
|     [public] | ||||
|     {% endif %} | ||||
|     {{group.display_name }} | ||||
|     <small>{{ group.fqn }}</small> | ||||
| </h2> | ||||
|     {% if not current_user.profile or not current_user.profile.is_member_of(group) %} | ||||
|         {% if group.visibility == 'public' %} | ||||
| Join | ||||
|         {% else %} | ||||
| Request membership | ||||
|         {% endif %} | ||||
|     {% else %} | ||||
| Invitation form | ||||
|     {% endif %}<br> | ||||
|     {% if group.details_visible_to(current_user.profile) %} | ||||
| <h2>{% trans %}Members{% endtrans %}</h2> | ||||
|         {% for member in group.members %} | ||||
| {{ profile_link(member) }} | ||||
|         {% endfor %} | ||||
|     {% else %} | ||||
| The details of this grop are not visible to you. | ||||
|     {% endif %} | ||||
| {% endblock content %} | ||||
							
								
								
									
										11
									
								
								calsocial/templates/group/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								calsocial/templates/group/list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| {% extends 'base.html' %} | ||||
|  | ||||
| {% block content %} | ||||
| <h2>{% trans %}Groups{% endtrans %}</h2> | ||||
| {% for group in groups %} | ||||
| <a href="{{ group.url }}">{{ group }}</a> | ||||
| {% else %} | ||||
| {% trans %}No groups.{% endtrans %} | ||||
| <a href="{{ url_for('group.create') }}">{% trans %}Do you want to create one?{% endtrans %} | ||||
| {% endfor %} | ||||
| {% endblock content %} | ||||
| @@ -11,10 +11,10 @@ | ||||
|         </tr> | ||||
|         <tr class="month"> | ||||
|             <td> | ||||
|                 <a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a> | ||||
|                 <a href="{{ url_for('hello', date=calendar.prev_year.timestamp()) }}">« {{ calendar.prev_year_year }}</a> | ||||
|             </td> | ||||
|             <td> | ||||
|                 <a href="{{ url_for('hello', date=calendar.prev_month) }}">‹ {{ calendar.prev_month_name }}</a> | ||||
|                 <a href="{{ url_for('hello', date=calendar.prev_month.timestamp()) }}">‹ {{ calendar.prev_month_name }}</a> | ||||
|             </td> | ||||
|             <td colspan="3" class="month-name"> | ||||
| {% if not calendar.has_today %} | ||||
| @@ -26,10 +26,10 @@ | ||||
| {% endif %} | ||||
|             </td> | ||||
|             <td> | ||||
|                 <a href="{{ url_for('hello', date=calendar.next_month) }}">{{ calendar.next_month_name }} ›</a> | ||||
|                 <a href="{{ url_for('hello', date=calendar.next_month.timestamp()) }}">{{ calendar.next_month_name }} ›</a> | ||||
|             </td> | ||||
|             <td> | ||||
|                 <a href="{{ url_for('hello', date=calendar.next_year) }}">{{ calendar.next_year_year }} »</a> | ||||
|                 <a href="{{ url_for('hello', date=calendar.next_year.timestamp()) }}">{{ calendar.next_year_year }} »</a> | ||||
|             </td> | ||||
|         </tr> | ||||
|         <tr class="days"> | ||||
|   | ||||
| @@ -1,14 +1,18 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% from '_macros.html' import profile_link %} | ||||
|  | ||||
| {% block content %} | ||||
| <h1> | ||||
| <h2 class="ui header"> | ||||
|     {% if profile.builtin_avatar %} | ||||
|     <img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular image"> | ||||
|     {% endif %} | ||||
|     {% if profile.locked %} | ||||
|     <i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i> | ||||
|     <span class="sr-only">{% trans %}locked profile{% endtrans %}</span> | ||||
|     {% endif %} | ||||
|     {{ profile.display_name }} | ||||
|     <small>@{{ profile.user.username}}</small> | ||||
| </h1> | ||||
| </h2> | ||||
|     {% if profile.user != current_user %} | ||||
| <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> | ||||
|     {% endif %} | ||||
| @@ -18,7 +22,7 @@ | ||||
| </h2> | ||||
|  | ||||
|     {% for followed in profile.followed_list %} | ||||
| {{ followed }} | ||||
| {{ profile_link(followed) }} | ||||
|     {% endfor %} | ||||
|  | ||||
| <h2> | ||||
| @@ -26,6 +30,6 @@ | ||||
| </h2> | ||||
|  | ||||
|     {% for follower in profile.follower_list %} | ||||
| {{ follower }} | ||||
| {{ profile_link(follower) }} | ||||
|     {% endfor %} | ||||
| {% endblock content %} | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% from '_macros.html' import profile_link %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="ui grid"> | ||||
| @@ -72,12 +73,10 @@ | ||||
|         </div> | ||||
|  | ||||
|         <div class="four wide column"> | ||||
|     {% if admin_profile %} | ||||
|             <h2>{% trans %}Administered by{% endtrans %}</h2> | ||||
|             <a href="#" class="ui profile"> | ||||
|                 <div class="avatar"></div> | ||||
|                 <div class="display name">Your Admin here</div> | ||||
|                 <div class="handle">@admin@he.re</div> | ||||
|             </a> | ||||
|             {{ profile_link(admin_profile) }} | ||||
|     {% endif %} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
							
								
								
									
										62
									
								
								tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| """Helper functions and fixtures for testing | ||||
| """ | ||||
|  | ||||
| from contextlib import contextmanager | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from helpers import configure_app | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def client(): | ||||
|     """Fixture that provides a Flask test client | ||||
|     """ | ||||
|  | ||||
|     from calsocial import app | ||||
|     from calsocial.models import db | ||||
|  | ||||
|     configure_app() | ||||
|     client = app.test_client() | ||||
|  | ||||
|     with app.app_context(): | ||||
|         db.create_all() | ||||
|  | ||||
|     yield client | ||||
|  | ||||
|     with app.app_context(): | ||||
|         db.drop_all() | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def database(): | ||||
|     """Fixture to provide all database tables in an active application context | ||||
|     """ | ||||
|  | ||||
|     from calsocial import app | ||||
|     from calsocial.models import db | ||||
|  | ||||
|     configure_app() | ||||
|  | ||||
|     with app.app_context(): | ||||
|         db.create_all() | ||||
|  | ||||
|         yield db | ||||
|  | ||||
|         db.drop_all() | ||||
| @@ -14,13 +14,11 @@ | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| """Helper functions and fixtures for testing | ||||
| """Helper functions for testing | ||||
| """ | ||||
|  | ||||
| from contextlib import contextmanager | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| import calsocial | ||||
| from calsocial.models import db | ||||
|  | ||||
| @@ -34,22 +32,6 @@ def configure_app(): | ||||
|     calsocial.app.config['WTF_CSRF_ENABLED'] = False | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def client(): | ||||
|     """Fixture that provides a Flask test client | ||||
|     """ | ||||
|     configure_app() | ||||
|     client = calsocial.app.test_client() | ||||
|  | ||||
|     with calsocial.app.app_context(): | ||||
|         db.create_all() | ||||
|  | ||||
|     yield client | ||||
|  | ||||
|     with calsocial.app.app_context(): | ||||
|         db.drop_all() | ||||
|  | ||||
|  | ||||
| def login(client, username, password, no_redirect=False): | ||||
|     """Login with the specified username and password | ||||
|     """ | ||||
| @@ -59,21 +41,6 @@ def login(client, username, password, no_redirect=False): | ||||
|                        follow_redirects=not no_redirect) | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def database(): | ||||
|     """Fixture to provide all database tables in an active application context | ||||
|     """ | ||||
|  | ||||
|     configure_app() | ||||
|  | ||||
|     with calsocial.app.app_context(): | ||||
|         db.create_all() | ||||
|  | ||||
|         yield db | ||||
|  | ||||
|         db.drop_all() | ||||
|  | ||||
|  | ||||
| @contextmanager | ||||
| def alter_config(app, **kwargs): | ||||
|     saved = {} | ||||
|   | ||||
							
								
								
									
										49
									
								
								tests/test_app_state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								tests/test_app_state.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| # 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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| def test_app_state_set(database): | ||||
|     from calsocial.models import db, AppState | ||||
|  | ||||
|     AppState['test'] = 'value' | ||||
|  | ||||
|     state = AppState.query \ | ||||
|                     .filter(AppState.env == 'testing') \ | ||||
|                     .filter(AppState.key == 'test') \ | ||||
|                     .one() | ||||
|  | ||||
|     assert state.value == 'value' | ||||
|  | ||||
|  | ||||
| def test_app_state_get(database): | ||||
|     from calsocial.models import db, AppState | ||||
|  | ||||
|     state = AppState(env='testing', key='test', value='value') | ||||
|     db.session.add(state) | ||||
|     db.session.commit() | ||||
|  | ||||
|     assert AppState['test'] == 'value' | ||||
|  | ||||
|  | ||||
| def test_app_state_setdefault(database): | ||||
|     from calsocial.models import AppState | ||||
|  | ||||
|     AppState['test'] = 'value' | ||||
|     AppState.setdefault('test', 'new value') | ||||
|  | ||||
|     assert AppState['test'] == 'value' | ||||
|  | ||||
|     AppState.setdefault('other_test', 'value') | ||||
|     assert AppState['other_test'] == 'value' | ||||
| @@ -17,8 +17,9 @@ | ||||
| """General tests for Calendar.social | ||||
| """ | ||||
|  | ||||
| from helpers import client | ||||
| from flask import current_app | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| def test_index_no_login(client): | ||||
|     """Test the main page without logging in | ||||
| @@ -26,3 +27,50 @@ def test_index_no_login(client): | ||||
|  | ||||
|     page = client.get('/') | ||||
|     assert b'Peek inside' in page.data | ||||
|  | ||||
|  | ||||
| def test_instance_adin_unset(database): | ||||
|     """Test the instance admin feature if the admin is not set | ||||
|     """ | ||||
|  | ||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'): | ||||
|         assert current_app.instance_admin is None | ||||
|  | ||||
|  | ||||
| def test_instance_admin_bad_value(database): | ||||
|     """Test the instance admin feature if the value is invalid | ||||
|     """ | ||||
|  | ||||
|     from calsocial.models import AppState | ||||
|  | ||||
|     AppState['instance_admin'] = 'value' | ||||
|  | ||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'): | ||||
|         assert current_app.instance_admin is None | ||||
|  | ||||
|  | ||||
| def test_instance_admin_doesnot_exist(database): | ||||
|     """Test the instance admin feature if the admin user does not exist | ||||
|     """ | ||||
|  | ||||
|     from calsocial.models import AppState | ||||
|  | ||||
|     AppState['instance_admin'] = '0' | ||||
|  | ||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'): | ||||
|         assert current_app.instance_admin is None | ||||
|  | ||||
|  | ||||
| def test_instance_admin(database): | ||||
|     """Test the instance admin feature if the admin user does not exist | ||||
|     """ | ||||
|  | ||||
|     from calsocial.models import db, AppState, User | ||||
|  | ||||
|     user = User(username='admin') | ||||
|     db.session.add(user) | ||||
|     db.session.commit() | ||||
|  | ||||
|     AppState['instance_admin'] = user.id | ||||
|  | ||||
|     assert current_app.instance_admin == user | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
| import calsocial | ||||
| from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow | ||||
|  | ||||
| from helpers import client, database, login | ||||
| from helpers import login | ||||
|  | ||||
|  | ||||
| def test_profile_follow(database): | ||||
|   | ||||
							
								
								
									
										92
									
								
								tests/test_gregorian.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								tests/test_gregorian.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| from datetime import datetime, date | ||||
|  | ||||
| from pytz import utc | ||||
|  | ||||
| from calsocial.calendar_system.gregorian import GregorianCalendar | ||||
|  | ||||
|  | ||||
| def test_day_list(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.days[0].date() == date(2018, 1, 1) | ||||
|     assert calendar.days[-1].date() == date(2018, 2, 4) | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.days[0].date() == date(2018, 11, 26) | ||||
|     assert calendar.days[-1].date() == date(2019, 1, 6) | ||||
|  | ||||
|  | ||||
| def test_prev_year(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_year == datetime(2017, 1, 1, 0, 0, 0) | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_year == datetime(2017, 12, 1, 0, 0, 0) | ||||
|  | ||||
|  | ||||
| def test_prev_year_year(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_year_year == 2017 | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_year_year == 2017 | ||||
|  | ||||
|  | ||||
| def test_prev_month(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_month == datetime(2017, 12, 1, 0, 0, 0) | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_month == datetime(2018, 11, 1, 0, 0, 0) | ||||
|  | ||||
|  | ||||
| def test_prev_month_name(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_month_name == 'December' | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.prev_month_name == 'November' | ||||
|  | ||||
|  | ||||
| def test_next_year(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_year == datetime(2019, 1, 1, 0, 0, 0) | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_year == datetime(2019, 12, 1, 0, 0, 0) | ||||
|  | ||||
|  | ||||
| def test_next_year_year(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_year_year == 2019 | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_year_year == 2019 | ||||
|  | ||||
|  | ||||
| def test_next_month(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_month == datetime(2018, 2, 1, 0, 0, 0) | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_month == datetime(2019, 1, 1, 0, 0, 0) | ||||
|  | ||||
|  | ||||
| def test_next_month_name(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_month_name == 'February' | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.next_month_name == 'January' | ||||
|  | ||||
|  | ||||
| def test_has_today(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(1990, 12, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.has_today is False | ||||
|  | ||||
|     calendar = GregorianCalendar(utc.localize(datetime.utcnow()).timestamp()) | ||||
|     assert calendar.has_today is True | ||||
|  | ||||
|  | ||||
| def test_current_month(): | ||||
|     calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp()) | ||||
|     assert calendar.month == 'January, 2018' | ||||
| @@ -20,7 +20,7 @@ | ||||
| import calsocial | ||||
| from calsocial.models import db, User | ||||
|  | ||||
| from helpers import client, login | ||||
| from helpers import login | ||||
|  | ||||
|  | ||||
| def test_login_invalid_user(client): | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
| import calsocial | ||||
| from calsocial.models import db, User | ||||
|  | ||||
| from helpers import alter_config, client | ||||
| from helpers import alter_config | ||||
|  | ||||
|  | ||||
| def test_register_page(client): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user