1 Commits

Author SHA1 Message Date
60ad2c7ae2 Add Vagrant related files 2018-07-23 11:59:13 +02:00
48 changed files with 455 additions and 1197 deletions

View File

@@ -1 +0,0 @@
FLASK_ENV=testing

4
.gitignore vendored
View File

@@ -4,5 +4,5 @@ __pycache__/
/calsocial/translations/*/LC_MESSAGES/*.mo
/.pytest_cache/
/.env
/.coverage
/htmlcov/
/.vagrant/
/ansible/*.retry

View File

@@ -18,7 +18,6 @@ flask-caching = "*"
[dev-packages]
pylint = "*"
pytest = "*"
pytest-cov = "*"
[requires]
python_version = "3.6"

66
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294"
"sha256": "e4313bc9baef5cb187176951d45094fe1de4ccba0d15ab58efbac21b6434f255"
},
"pipfile-spec": 6,
"requires": {
@@ -284,10 +284,10 @@
"develop": {
"astroid": {
"hashes": [
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f"
"sha256:8704779744963d56a2625ec2949eb150bd499fc099510161ddbb2b64e2d98138",
"sha256:add3fd690e7c1fe92436d17be461feeaa173e6f33e0789734310334da0f30027"
],
"version": "==2.0.1"
"version": "==2.0"
},
"atomicwrites": {
"hashes": [
@@ -303,50 +303,6 @@
],
"version": "==18.1.0"
},
"coverage": {
"hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
],
"version": "==4.5.1"
},
"isort": {
"hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
@@ -421,11 +377,11 @@
},
"pylint": {
"hashes": [
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251"
"sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4",
"sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8"
],
"index": "pypi",
"version": "==2.0.1"
"version": "==2.0.0"
},
"pytest": {
"hashes": [
@@ -435,14 +391,6 @@
"index": "pypi",
"version": "==3.6.3"
},
"pytest-cov": {
"hashes": [
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
],
"index": "pypi",
"version": "==2.5.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",

74
Vagrantfile vendored Normal file
View File

@@ -0,0 +1,74 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = 'fedora/28-cloud-base'
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
config.vm.synced_folder './', '/vagrant', type: 'sshfs'
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "ansible_local" do |ansible|
ansible.compatibility_mode = '2.0'
ansible.install = true
ansible.provisioning_path = '/vagrant/ansible'
ansible.playbook = 'dev.yml'
end
end

23
ansible/dev.yml Normal file
View File

@@ -0,0 +1,23 @@
---
- name: Configuration for local development on Vagrant
hosts: all
become: yes
vars:
user_name: vagrant
group_name: vagrant
roles:
- common
- python
- role: gunicorn
autostart: false
enabled: false
- role: nginx
use_ssl: false
enabled: false
tasks:
- name: Allow virtualenv python to bind to port 80
command: setcap cap_net_bind_service=ep /usr/bin/python3.6
changed_when: false

12
ansible/install.sh Normal file
View File

@@ -0,0 +1,12 @@
#! /usr/bin/env bash
if [ ! -f /etc/ansible/hosts ]
then
echo "Installing Ansible..."
sudo dnf remove ansible
sudo dnf install ansible-python3
printf 'localhost\n' | sudo tee /etc/ansible/hosts > /dev/null
fi
echo "Ansible is installed."

View File

@@ -0,0 +1,8 @@
---
- name: Install required packages
dnf:
name: "{{ item }}"
state: present
with_items:
- libselinux-python

View File

@@ -0,0 +1,10 @@
---
# Project name
project_name: calendar.social
# Project path
project_path: /vagrant
# Flask app path
application_path: /vagrant/app

View File

@@ -0,0 +1,56 @@
---
- name: Install Supervisor
dnf:
name: "{{ item }}"
state: present
with_items:
- supervisor
- name: Start supervisord
service:
name: supervisord
state: restarted
- name: Create the Gunicorn config directory
file:
path: /etc/gunicorn
state: directory
owner: "{{ user_name }}"
group: "{{ group_name }}"
mode: 0700
- name: Create the Gunicorn config file in /etc/gunicorn
template:
src: gunicorn.conf.j2
dest: /etc/gunicorn/gunicorn.conf
- name: Create the Gunicorn log directory
file:
path: /var/log/gunicorn
state: directory
owner: "{{ user_name }}"
group: "{{ group_name }}"
mode: 0700
- name: Create the Supervisor config file for Gunicorn
template:
src: supervisor.conf.j2
dest: /etc/supervisord.d/gunicorn.ini
- name: Re-read the Supervisor config files
supervisorctl:
name: gunicorn
state: present
- name: Start Gunicorn with supervisord
supervisorctl:
name: gunicorn
state: restarted
when: enabled
- name: Stop Gunicorn for local dev
supervisorctl:
name: gunicorn
state: stopped
when: not enabled

View File

@@ -0,0 +1,9 @@
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
proc_name = 'gunicorn'
bind = '127.0.0.1:8000'
errorlog = '/var/log/gunicorn/gunicorn-error.log'
accesslog = '/var/log/gunicorn/gunicorn-access.log'
loglevel = 'warning'
timeout = 60

View File

@@ -0,0 +1,8 @@
[program:gunicorn]
command=pipenv run gunicorn wsgi:app -c /etc/gunicorn/gunicorn.conf --pythonpath {{ application_path }}
directory={{ application_path }}
user={{ user_name }}
group={{ group_name }}
autorestart=true
autostart={{ autostart | bool | lower }}
redirect_stderr=true

View File

@@ -0,0 +1,11 @@
---
- name: Reload Nginx
service:
name: nginx
state: reloaded
- name: Stop Nginx
service:
name: nginx
state: stopped

View File

@@ -0,0 +1,42 @@
---
- name: Install Nginx
dnf:
name: "{{ item }}"
state: present
with_items:
- nginx
- name: Create the Nginx configuration file for SSL
template:
src: site-ssl.conf.j2
dest: /etc/nginx/conf.d/{{ project_name }}-ssl.conf
when: use_ssl
notify: Reload Nginx
- name: Create the Nginx configuration file (non-SSL)
template:
src: site.conf.j2
dest: /etc/nginx/conf.d/{{ project_name }}.conf
when: not use_ssl
notify: Reload Nginx
- name: Ensure that the default site is removed
file:
path: /etc/nginx/conf.d/default.conf
state: absent
- name: Ensure Nginx service is started, enable service on restart
service:
name: nginx
state: restarted
enabled: yes
when: enabled
- name: Stop nginx for local dev, disable service
service:
name: nginx
state: stopped
enabled: no
notify: Stop Nginx
when: not enabled

View File

@@ -0,0 +1,41 @@
upstream appserver {
server localhost:8000 fail_timeout=0;
}
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl deferred;
server_name {{ host_name }};
ssl_certificate {{ home_path }}/{{ project_name }}.crt;
ssl_certificate_key {{ home_path }}/{{ project_name }}.key;
ssl_session_cache shared:SSL:32m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/{{ project_name }}.access.log;
error_log /var/log/nginx/{{ project_name }}.error.log info;
keepalive_timeout 5;
location /static {
alias {{ project_path }}/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_read_timeout 180s;
if (!-f $request_filename) {
proxy_pass http://appserver;
break;
}
}
}

View File

@@ -0,0 +1,29 @@
upstream appserver {
server localhost:8000 fail_timeout=0;
}
server {
listen 80;
server_name {{ host_name }};
access_log /var/log/nginx/{{ project_name }}.access.log;
error_log /var/log/nginx/{{ project_name }}.error.log info;
keepalive_timeout 5;
location /static {
alias {{ project_path }}/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_read_timeout 180s;
if (-f $request_filename) {
proxy_pass http://appserver;
break;
}
}
}

View File

@@ -0,0 +1,3 @@
---
host_name: calendar-social.local

View File

@@ -0,0 +1,22 @@
---
- name: Install common python packages
dnf:
name: "{{ item }}"
state: present
with_items:
- pipenv
- name: Delete Python cache files
command: find . -type d -name __pycache__ -exec rm -r {} +
args:
chdir: "{{ project_path }}"
changed_when: false
- name: Install packages
command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy
- name: Install development related packages
command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy --dev
args:
chdir: "{{ project_path }}"

View File

@@ -19,10 +19,8 @@
from datetime import datetime
import os
from warnings import warn
from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \
url_for
from flask import Flask, abort, current_app, 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
@@ -74,22 +72,12 @@ class CalendarSocialApp(Flask, RoutedMixin):
self._timezone = None
config_name = os.environ.get('FLASK_ENV', config or 'development')
config_name = os.environ.get('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)
@@ -119,7 +107,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
if current_user.is_authenticated and \
not current_user.profile and \
request.endpoint != 'account.first_steps':
request.endpoint != 'first_steps':
return redirect(url_for('account.first_steps'))
return None
@@ -129,6 +117,9 @@ 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
@@ -147,32 +138,6 @@ 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
@@ -201,16 +166,13 @@ 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,
admin_profile=admin_profile)
event_count=event_count)
@RoutedMixin.route('/')
def hello(self):

View File

@@ -6,4 +6,4 @@ from calsocial import CalendarSocialApp
app = CalendarSocialApp('calsocial')
app.run()
app.run(host='0.0.0.0', port=80)

View File

@@ -46,7 +46,7 @@ class AccountBlueprint(Blueprint, RoutedMixin):
app.register_blueprint(self, url_prefix=url_prefix)
@staticmethod
@RoutedMixin.route('/register', methods=['POST', 'GET'])
@RoutedMixin.route('/register')
def register_account():
"""View for user registration

View File

@@ -1,60 +0,0 @@
# 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})

View File

@@ -21,7 +21,7 @@ from datetime import timedelta
import pickle
from uuid import uuid4
from flask import has_request_context, request as flask_request, session as flask_session
from flask import current_app, has_request_context, request, session
from flask.sessions import SessionInterface, SessionMixin
from flask_caching import Cache
from werkzeug.datastructures import CallbackDict
@@ -46,7 +46,7 @@ class CachedSession(CallbackDict, SessionMixin): # pylint: disable=too-many-anc
self.__modifying = True
if has_request_context():
self['ip'] = flask_request.remote_addr
self['ip'] = request.remote_addr
self.modified = True
@@ -59,9 +59,6 @@ class CachedSession(CallbackDict, SessionMixin): # pylint: disable=too-many-anc
@property
def user(self):
"""The user this session belongs to
"""
from calsocial.models import User
if 'user_id' not in self:
@@ -90,11 +87,11 @@ class CachedSessionInterface(SessionInterface):
return str(uuid4())
@staticmethod
def get_cache_expiration_time(app, sess):
def get_cache_expiration_time(app, session):
"""Get the expiration time of the cache entry
"""
if sess.permanent:
if session.permanent:
return app.permanent_session_lifetime
return timedelta(days=1)
@@ -150,10 +147,7 @@ class CachedSessionInterface(SessionInterface):
domain=domain)
def delete_session(self, sid):
"""Delete the session with ``sid`` as its session ID
"""
if has_request_context() and flask_session.sid == sid:
if has_request_context() and session.sid == sid:
raise ValueError('Will not delete the current session')
cache.delete(self.prefix + sid)

View File

@@ -18,12 +18,24 @@
"""
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
"""
@@ -92,6 +104,7 @@ class GregorianCalendar(CalendarSystem):
return day_list
@property
@to_timestamp
def prev_year(self):
"""Returns the timestamp of the same date in the previous year
"""
@@ -106,6 +119,7 @@ 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
"""
@@ -128,6 +142,7 @@ 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
"""
@@ -150,6 +165,7 @@ 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
"""
@@ -182,7 +198,7 @@ class GregorianCalendar(CalendarSystem):
month_end_timestamp = month_start_timestamp.replace(month=next_month)
return month_start_timestamp <= now < month_end_timestamp
return now >= month_start_timestamp and now < month_end_timestamp
@staticmethod
def day_events(date, user=None):
@@ -203,7 +219,7 @@ class GregorianCalendar(CalendarSystem):
end_timestamp = start_timestamp + timedelta(days=1)
events = events.filter((Event.start_time <= end_timestamp) &
(Event.end_time >= start_timestamp)) \
(Event.end_time >= start_timestamp)) \
.order_by('start_time', 'end_time')
if user is None:

View File

@@ -1,18 +0,0 @@
"""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'

View File

@@ -23,7 +23,7 @@ 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, RadioField
from wtforms import BooleanField, PasswordField, SelectField, StringField
from wtforms.ext.dateutil.fields import DateTimeField
from wtforms.validators import DataRequired, Email, StopValidation, ValidationError
from wtforms.widgets import TextArea
@@ -394,19 +394,11 @@ 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):
from flask import current_app
kwargs.update(
{
'display_name': profile.display_name,
'locked': profile.locked,
'builtin_avatar': profile.builtin_avatar,
})
kwargs.update({'display_name': profile.display_name})
kwargs.update({'locked': profile.locked})
FlaskForm.__init__(self, *args, **kwargs)
self.builtin_avatar.choices = [(name, name) for name in current_app.config['BUILTIN_AVATARS']]
self.profile = profile

View File

@@ -21,14 +21,12 @@ 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
@@ -70,18 +68,6 @@ class NotificationAction(Enum):
#: A user has been invited to an event
invite = 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
return Enum.__eq__(self, other)
NOTIFICATION_ACTION_MESSAGES = {
NotificationAction.follow: (_('%(actor)s followed you'), _('%(actor)s followed %(item)s')),
@@ -135,80 +121,6 @@ EVENT_VISIBILITY_TRANSLATIONS = {
}
class ResponseVisibility(Enum):
"""Enumeration for response visibility
"""
#: The response is only visible to the invitee
private = 0
#: The response is only visible to the event organisers
organisers = 1
#: The response is only visible to the event attendees
attendees = 2
#: The response is visible to the invitees friends (ie. mutual follows)
friends = 3
#: The response is visible to the invitees followers
followers = 4
#: The response is visible to anyone
public = 5
RESPONSE_VISIBILITY_TRANSLATIONS = {
ResponseVisibility.private: _('Visible only to myself'),
ResponseVisibility.organisers: _('Visible only to event organisers'),
ResponseVisibility.attendees: _('Visible only to event attendees'),
ResponseVisibility.friends: _('Visible only to my friends'),
ResponseVisibility.followers: _('Visible only to my followers'),
ResponseVisibility.public: _('Visible to anyone'),
}
class EventAvailability(Enum):
"""Enumeration of event availabilities
"""
free = 0
busy = 1
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
return Enum.__eq__(self, other)
class UserAvailability(Enum):
"""Enumeration of user availabilities
"""
free = 0
busy = 1
tentative = 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
return Enum.__eq__(self, other)
class SettingsProxy:
"""Proxy object to get settings for a user
"""
@@ -294,6 +206,7 @@ class User(db.Model, UserMixin):
If the user didnt set a time zone yet, the application default is used.
"""
from flask import current_app
from pytz import timezone
from pytz.exceptions import UnknownTimeZoneError
@@ -372,9 +285,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
#: If locked, a profile cannot be followed without the owners 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
@@ -490,45 +400,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return notification
def is_following(self, profile):
"""Check if this profile is following ``profile``
"""
try:
UserFollow.query.filter(UserFollow.follower == self).filter(UserFollow.followed == profile).one()
except NoResultFound:
return False
return True
def is_friend_of(self, profile):
"""Check if this profile is friends with ``profile``
"""
reverse = db.aliased(UserFollow)
try:
UserFollow.query \
.filter(UserFollow.follower == self) \
.join(reverse, UserFollow.followed_id == reverse.follower_id) \
.filter(UserFollow.follower_id == reverse.followed_id) \
.one()
except NoResultFound:
return False
return True
def is_attending(self, event):
"""Check if this profile is attending ``event``
"""
try:
Response.query.filter(Response.profile == self).filter(Response.event == event).one()
except NoResultFound:
return False
return True
class Event(db.Model):
"""Database model for events
@@ -915,112 +786,3 @@ class Response(db.Model): # pylint: disable=too-few-public-methods
#: The response itself
response = db.Column(db.Enum(ResponseType), nullable=False)
#: The visibility of the response
visibility = db.Column(db.Enum(ResponseVisibility), nullable=False)
def visible_to(self, profile):
"""Checks if the response can be visible to ``profile``.
:param profile: the profile looking at the response. If None, it is viewed as anonymous
:type profile: Profile, None
:returns: ``True`` if the response should be visible, ``False`` otherwise
:rtype: bool
"""
if self.profile == profile:
return True
if self.visibility == ResponseVisibility.private:
return False
if self.visibility == ResponseVisibility.organisers:
return profile == self.event.profile
if self.visibility == ResponseVisibility.attendees:
return profile is not None and \
(profile.is_attending(self.event) or \
profile == self.event.profile)
# From this point on, if the event is not public, only attendees can see responses
if self.event.visibility != EventVisibility.public:
return profile is not None and \
(profile.is_attending(self.event) or
profile == self.event.profile)
if self.visibility == ResponseVisibility.friends:
return profile is not None and \
(profile.is_friend_of(self.profile) or \
profile.is_attending(self.event) or \
profile == self.event.profile or \
profile == self.profile)
if self.visibility == ResponseVisibility.followers:
return profile is not None and \
(profile.is_following(self.profile) or \
profile.is_attending(self.event) or \
profile == self.event.profile or \
profile == self.profile)
return self.visibility == ResponseVisibility.public
class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-methods
"""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}"'

View File

@@ -1,35 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,91 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,39 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -1,11 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,26 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -18,13 +18,3 @@
{% 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 %}

View File

@@ -13,7 +13,6 @@
<br>
{% endif %}
{{ field(form.builtin_avatar) }}
{{ field(form.display_name) }}
{{ field(form.locked, inline=true) }}

View File

@@ -11,10 +11,10 @@
</tr>
<tr class="month">
<td>
<a href="{{ url_for('hello', date=calendar.prev_year.timestamp()) }}">« {{ calendar.prev_year_year }}</a>
<a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a>
</td>
<td>
<a href="{{ url_for('hello', date=calendar.prev_month.timestamp()) }}"> {{ calendar.prev_month_name }}</a>
<a href="{{ url_for('hello', date=calendar.prev_month) }}"> {{ 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.timestamp()) }}">{{ calendar.next_month_name }} </a>
<a href="{{ url_for('hello', date=calendar.next_month) }}">{{ calendar.next_month_name }} </a>
</td>
<td>
<a href="{{ url_for('hello', date=calendar.next_year.timestamp()) }}">{{ calendar.next_year_year }} »</a>
<a href="{{ url_for('hello', date=calendar.next_year) }}">{{ calendar.next_year_year }} »</a>
</td>
</tr>
<tr class="days">

View File

@@ -1,18 +1,14 @@
{% extends 'base.html' %}
{% from '_macros.html' import profile_link %}
{% block content %}
<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 %}
<h1>
{% 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>
</h2>
</h1>
{% if profile.user != current_user %}
<a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a>
{% endif %}
@@ -22,7 +18,7 @@
</h2>
{% for followed in profile.followed_list %}
{{ profile_link(followed) }}
{{ followed }}
{% endfor %}
<h2>
@@ -30,6 +26,6 @@
</h2>
{% for follower in profile.follower_list %}
{{ profile_link(follower) }}
{{ follower }}
{% endfor %}
{% endblock content %}

View File

@@ -1,5 +1,4 @@
{% extends 'base.html' %}
{% from '_macros.html' import profile_link %}
{% block content %}
<div class="ui grid">
@@ -73,10 +72,12 @@
</div>
<div class="four wide column">
{% if admin_profile %}
<h2>{% trans %}Administered by{% endtrans %}</h2>
{{ profile_link(admin_profile) }}
{% endif %}
<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>
</div>
</div>
</div>

View File

@@ -89,9 +89,6 @@ class RoutedMixin:
"""
def register_routes(self):
"""Register all routes that were marked with :meth:`route`
"""
for attr_name in self.__dir__():
attr = getattr(self, attr_name)

View File

@@ -1,62 +0,0 @@
# 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()

View File

@@ -14,10 +14,10 @@
# 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 for testing
"""Helper functions and fixtures for testing
"""
from contextlib import contextmanager
import pytest
import calsocial
from calsocial.models import db
@@ -32,6 +32,22 @@ 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
"""
@@ -41,20 +57,16 @@ def login(client, username, password, no_redirect=False):
follow_redirects=not no_redirect)
@contextmanager
def alter_config(app, **kwargs):
saved = {}
@pytest.fixture
def database():
"""Fixture to provide all database tables in an active application context
"""
for key, value in kwargs.items():
if key in app.config:
saved[key] = app.config[key]
configure_app()
app.config[key] = value
with calsocial.app.app_context():
db.create_all()
yield
yield db
for key, value in kwargs.items():
if key in saved:
app.config[key] = saved[key]
else:
del app.config[key]
db.drop_all()

View File

@@ -1,49 +0,0 @@
# 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'

View File

@@ -17,60 +17,12 @@
"""General tests for Calendar.social
"""
from flask import current_app
from helpers import client
import pytest
def test_index_no_login(client):
"""Test the main page without logging in
"""
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
assert b'Welcome to Calendar.social' in page.data

View File

@@ -20,7 +20,7 @@
import calsocial
from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow
from helpers import login
from helpers import client, database, login
def test_profile_follow(database):

View File

@@ -1,92 +0,0 @@
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'

View File

@@ -20,7 +20,15 @@
import calsocial
from calsocial.models import db, User
from helpers import login
from helpers import client, login
def test_index_no_login(client):
"""Test the main page without logging in
"""
page = client.get('/')
assert b'Welcome to Calendar.social' in page.data
def test_login_invalid_user(client):
@@ -73,4 +81,4 @@ def test_login_first_steps(client):
assert page.location == 'http://localhost/'
page = client.get('/')
assert page.location == 'http://localhost/accounts/first-steps'
assert page.location == 'http://localhost/first-steps'

View File

@@ -20,28 +20,28 @@
import calsocial
from calsocial.models import db, User
from helpers import alter_config
from helpers import client
def test_register_page(client):
"""Test the registration page
"""
page = client.get('/accounts/register')
page = client.get('/register')
assert b'Register</button>' in page.data
def test_register_post_empty(client):
"""Test sending empty registration data
"""
page = client.post('/accounts/register', data={})
page = client.post('/register', data={})
assert b'This field is required' in page.data
def test_register_invalid_email(client):
"""Test sending an invalid email address
"""
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test',
'password': 'password',
@@ -53,7 +53,7 @@ def test_register_password_mismatch(client):
"""Test sending different password for registration
"""
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
@@ -65,12 +65,13 @@ def test_register(client):
"""Test user registration
"""
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
print(page.data)
assert page.status_code == 302
assert page.location == 'http://localhost/'
@@ -89,7 +90,7 @@ def test_register_existing_username(client):
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
@@ -106,16 +107,10 @@ def test_register_existing_email(client):
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'tester',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This email address can not be used' in page.data
def test_registration_disabled(client):
with alter_config(calsocial.app, REGISTRATION_ENABLED=False):
page = client.get('/accounts/register')
assert b'Registration is disabled' in page.data

View File

@@ -1,175 +0,0 @@
# 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/>.
"""Profile related tests for Calendar.social
"""
from datetime import datetime
from calsocial.models import db, Event, EventVisibility, Profile, Response, ResponseType, \
ResponseVisibility, UserFollow
from helpers import database
def test_response_visibility(database):
"""Test response visibility in different scenarios
"""
test_data = (
# Third element value descriptions:
# none=not logged in
# unknown=completely unrelated profile
# follower=spectator is following respondent
# friend=spectator and respondent are friends (mutual follow)
# attendee=spectator is an attendee of the event
# respondent=spectator is the respondent
(EventVisibility.public, ResponseVisibility.public, 'anon', True),
(EventVisibility.public, ResponseVisibility.public, 'unknown', True),
(EventVisibility.public, ResponseVisibility.public, 'follower', True),
(EventVisibility.public, ResponseVisibility.public, 'friend', True),
(EventVisibility.public, ResponseVisibility.public, 'attendee', True),
(EventVisibility.public, ResponseVisibility.public, 'organiser', True),
(EventVisibility.public, ResponseVisibility.public, 'respondent', True),
(EventVisibility.public, ResponseVisibility.followers, 'anon', False),
(EventVisibility.public, ResponseVisibility.followers, 'unknown', False),
(EventVisibility.public, ResponseVisibility.followers, 'follower', True),
(EventVisibility.public, ResponseVisibility.followers, 'friend', True),
(EventVisibility.public, ResponseVisibility.followers, 'attendee', True),
(EventVisibility.public, ResponseVisibility.followers, 'organiser', True),
(EventVisibility.public, ResponseVisibility.followers, 'respondent', True),
(EventVisibility.public, ResponseVisibility.friends, 'anon', False),
(EventVisibility.public, ResponseVisibility.friends, 'unknown', False),
(EventVisibility.public, ResponseVisibility.friends, 'follower', False),
(EventVisibility.public, ResponseVisibility.friends, 'friend', True),
(EventVisibility.public, ResponseVisibility.friends, 'attendee', True),
(EventVisibility.public, ResponseVisibility.friends, 'organiser', True),
(EventVisibility.public, ResponseVisibility.friends, 'respondent', True),
(EventVisibility.public, ResponseVisibility.attendees, 'anon', False),
(EventVisibility.public, ResponseVisibility.attendees, 'unknown', False),
(EventVisibility.public, ResponseVisibility.attendees, 'follower', False),
(EventVisibility.public, ResponseVisibility.attendees, 'friend', False),
(EventVisibility.public, ResponseVisibility.attendees, 'attendee', True),
(EventVisibility.public, ResponseVisibility.attendees, 'organiser', True),
(EventVisibility.public, ResponseVisibility.attendees, 'respondent', True),
(EventVisibility.public, ResponseVisibility.organisers, 'anon', False),
(EventVisibility.public, ResponseVisibility.organisers, 'unknown', False),
(EventVisibility.public, ResponseVisibility.organisers, 'follower', False),
(EventVisibility.public, ResponseVisibility.organisers, 'friend', False),
(EventVisibility.public, ResponseVisibility.organisers, 'attendee', False),
(EventVisibility.public, ResponseVisibility.organisers, 'organiser', True),
(EventVisibility.public, ResponseVisibility.organisers, 'respondent', True),
(EventVisibility.public, ResponseVisibility.private, 'anon', False),
(EventVisibility.public, ResponseVisibility.private, 'unknown', False),
(EventVisibility.public, ResponseVisibility.private, 'follower', False),
(EventVisibility.public, ResponseVisibility.private, 'friend', False),
(EventVisibility.public, ResponseVisibility.private, 'attendee', False),
(EventVisibility.public, ResponseVisibility.private, 'organiser', False),
(EventVisibility.public, ResponseVisibility.private, 'respondent', True),
(EventVisibility.private, ResponseVisibility.public, 'anon', False),
(EventVisibility.private, ResponseVisibility.public, 'unknown', False),
(EventVisibility.private, ResponseVisibility.public, 'follower', False),
(EventVisibility.private, ResponseVisibility.public, 'friend', False),
(EventVisibility.private, ResponseVisibility.public, 'attendee', True),
(EventVisibility.private, ResponseVisibility.public, 'organiser', True),
(EventVisibility.private, ResponseVisibility.public, 'respondent', True),
(EventVisibility.private, ResponseVisibility.followers, 'anon', False),
(EventVisibility.private, ResponseVisibility.followers, 'unknown', False),
(EventVisibility.private, ResponseVisibility.followers, 'follower', False),
(EventVisibility.private, ResponseVisibility.followers, 'friend', False),
(EventVisibility.private, ResponseVisibility.followers, 'attendee', True),
(EventVisibility.private, ResponseVisibility.followers, 'organiser', True),
(EventVisibility.private, ResponseVisibility.followers, 'respondent', True),
(EventVisibility.private, ResponseVisibility.friends, 'anon', False),
(EventVisibility.private, ResponseVisibility.friends, 'unknown', False),
(EventVisibility.private, ResponseVisibility.friends, 'follower', False),
(EventVisibility.private, ResponseVisibility.friends, 'friend', False),
(EventVisibility.private, ResponseVisibility.friends, 'attendee', True),
(EventVisibility.private, ResponseVisibility.friends, 'organiser', True),
(EventVisibility.private, ResponseVisibility.friends, 'respondent', True),
(EventVisibility.private, ResponseVisibility.attendees, 'anon', False),
(EventVisibility.private, ResponseVisibility.attendees, 'unknown', False),
(EventVisibility.private, ResponseVisibility.attendees, 'follower', False),
(EventVisibility.private, ResponseVisibility.attendees, 'friend', False),
(EventVisibility.private, ResponseVisibility.attendees, 'attendee', True),
(EventVisibility.private, ResponseVisibility.attendees, 'organiser', True),
(EventVisibility.private, ResponseVisibility.attendees, 'respondent', True),
(EventVisibility.private, ResponseVisibility.organisers, 'anon', False),
(EventVisibility.private, ResponseVisibility.organisers, 'unknown', False),
(EventVisibility.private, ResponseVisibility.organisers, 'follower', False),
(EventVisibility.private, ResponseVisibility.organisers, 'friend', False),
(EventVisibility.private, ResponseVisibility.organisers, 'attendee', False),
(EventVisibility.private, ResponseVisibility.organisers, 'organiser', True),
(EventVisibility.private, ResponseVisibility.organisers, 'respondent', True),
(EventVisibility.private, ResponseVisibility.private, 'anon', False),
(EventVisibility.private, ResponseVisibility.private, 'unknown', False),
(EventVisibility.private, ResponseVisibility.private, 'follower', False),
(EventVisibility.private, ResponseVisibility.private, 'friend', False),
(EventVisibility.private, ResponseVisibility.private, 'attendee', False),
(EventVisibility.private, ResponseVisibility.private, 'organiser', False),
(EventVisibility.private, ResponseVisibility.private, 'respondent', True),
)
for evt_vis, resp_vis, relation, exp_ret in test_data:
organiser = Profile(display_name='organiser')
event = Event(profile=organiser, visibility=evt_vis, title='Test Event', time_zone='UTC', start_time=datetime.utcnow(), end_time=datetime.utcnow())
respondent = Profile(display_name='Respondent')
response = Response(event=event, visibility=resp_vis, profile=respondent, response=ResponseType.going)
db.session.add_all([event, response])
if relation is 'anon':
spectator = None
elif relation == 'respondent':
spectator = respondent
elif relation == 'organiser':
spectator = organiser
else:
spectator = Profile(display_name='Spectator')
db.session.add(spectator)
if relation == 'follower' or relation == 'friend':
follow = UserFollow(follower=spectator, followed=respondent)
db.session.add(follow)
if relation == 'friend':
follow = UserFollow(follower=respondent, followed=spectator)
db.session.add(follow)
if relation == 'attendee':
att_response = Response(profile=spectator,
event=event,
response=ResponseType.going,
visibility=ResponseVisibility.public)
db.session.add(att_response)
db.session.commit()
notvis = ' not' if exp_ret else ''
assert_message = f'Response is{notvis} visible to {spectator} ({evt_vis}, {resp_vis}, {relation})'
assert response.visible_to(spectator) is exp_ret, assert_message