Compare commits

..

21 Commits

Author SHA1 Message Date
d635377d12 i18n: Add Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-07-30 22:28:26 +02:00
eaf71d4ce6 Add an empty profile for anonymous users
It evaluates to `None`, but at least it doesn’t raise an `AttributeError` now.
2018-07-26 21:50:12 +02:00
7cd2156cfc PyLint happiness (again) 2018-07-25 21:19:35 +02:00
b9c037f914 Display the instance admin on the welcome page 2018-07-25 21:07:47 +02:00
029d29ffb1 Make it possible to set the instance admin 2018-07-25 21:03:34 +02:00
4b1fff6544 Add the AppState model
This allows setting application state during run time
2018-07-25 20:27:13 +02:00
490474b2d6 Create a testing configuration
From now on, testing should be done with FLASK_ENV set to testing
2018-07-25 20:26:40 +02:00
bc67e692e0 [Refactor] Move test fixtures to conftest.py
…so they get automatically loaded
2018-07-25 20:25:12 +02:00
1e1e085ba4 [Bugfix] Use FLASK_ENV for the config filename instead of ENV 2018-07-25 20:25:12 +02:00
5996ae7079 Create a macro for profile links and use it on the welcome and profile display pages 2018-07-25 08:36:03 +02:00
3e5d8ee4d5 Make it possible to set one’s avatar
Only built-in avatars are supported yet.
2018-07-25 08:36:03 +02:00
c0c38ccb52 Make it possible to use builtin avatars
Images are from openclipart.org
2018-07-25 08:24:10 +02:00
c40e776036 Fully cover the Gregorian Calendar with tests
Being one of the cornerstones right now, it deserves a lot of tests
2018-07-24 08:28:04 +02:00
3deaa39256 [Test] Add test for disabled registration 2018-07-23 13:22:50 +02:00
c20b302458 Add the alter_config context manager for testing
It can temporarily change an app configuration value.
2018-07-23 13:22:50 +02:00
6f186c3a3f Add coverage related files to .gitignore 2018-07-23 13:22:50 +02:00
a97d884f42 Add pytest-cov as a developer dependency 2018-07-23 13:04:04 +02:00
11bd30e01f [Bugfix] Fix broken tests 2018-07-23 13:04:04 +02:00
4c3ec0564f [Bugfix] Fix registration
Allow POSTing to the registration endpoint
2018-07-23 13:04:04 +02:00
f8e3c748c0 [Bugfix] Fix jumping to first steps page upon initial login
This was missing from when the first steps view got moved to the accounts blueprint.
2018-07-23 13:04:04 +02:00
9e7ea29f5e [Lint] Make PyLint happy again 2018-07-23 12:35:57 +02:00
49 changed files with 1328 additions and 458 deletions

1
.env.testing Normal file
View File

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

4
.gitignore vendored
View File

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

View File

@ -18,6 +18,7 @@ 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": "e4313bc9baef5cb187176951d45094fe1de4ccba0d15ab58efbac21b6434f255"
"sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294"
},
"pipfile-spec": 6,
"requires": {
@ -284,10 +284,10 @@
"develop": {
"astroid": {
"hashes": [
"sha256:8704779744963d56a2625ec2949eb150bd499fc099510161ddbb2b64e2d98138",
"sha256:add3fd690e7c1fe92436d17be461feeaa173e6f33e0789734310334da0f30027"
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f"
],
"version": "==2.0"
"version": "==2.0.1"
},
"atomicwrites": {
"hashes": [
@ -303,6 +303,50 @@
],
"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",
@ -377,11 +421,11 @@
},
"pylint": {
"hashes": [
"sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4",
"sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8"
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251"
],
"index": "pypi",
"version": "==2.0.0"
"version": "==2.0.1"
},
"pytest": {
"hashes": [
@ -391,6 +435,14 @@
"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
View File

@ -1,74 +0,0 @@
# -*- 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

View File

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

View File

@ -1,12 +0,0 @@
#! /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

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

View File

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

View File

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

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

@ -1,8 +0,0 @@
[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

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

View File

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

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

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

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

View File

@ -1,22 +0,0 @@
---
- 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,8 +19,10 @@
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
@ -72,12 +74,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)
@ -107,7 +119,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
if current_user.is_authenticated and \
not current_user.profile and \
request.endpoint != 'first_steps':
request.endpoint != 'account.first_steps':
return redirect(url_for('account.first_steps'))
return None
@ -117,9 +129,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 +147,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 +201,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):

View File

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

View File

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

60
calsocial/app_state.py Normal file
View 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})

View File

@ -21,7 +21,7 @@ from datetime import timedelta
import pickle
from uuid import uuid4
from flask import current_app, has_request_context, request, session
from flask import has_request_context, request as flask_request, session as flask_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'] = request.remote_addr
self['ip'] = flask_request.remote_addr
self.modified = True
@ -59,6 +59,9 @@ 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:
@ -87,11 +90,11 @@ class CachedSessionInterface(SessionInterface):
return str(uuid4())
@staticmethod
def get_cache_expiration_time(app, session):
def get_cache_expiration_time(app, sess):
"""Get the expiration time of the cache entry
"""
if session.permanent:
if sess.permanent:
return app.permanent_session_lifetime
return timedelta(days=1)
@ -147,7 +150,10 @@ class CachedSessionInterface(SessionInterface):
domain=domain)
def delete_session(self, sid):
if has_request_context() and session.sid == sid:
"""Delete the session with ``sid`` as its session ID
"""
if has_request_context() and flask_session.sid == sid:
raise ValueError('Will not delete the current session')
cache.delete(self.prefix + sid)

View File

@ -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):

View 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'

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
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
@ -31,7 +31,7 @@ from wtforms.widgets import TextArea
from calsocial.models import EventVisibility, EVENT_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 +62,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 +394,20 @@ 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

View File

@ -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)
@ -206,7 +208,6 @@ 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
@ -285,6 +286,9 @@ 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
@ -786,3 +790,64 @@ 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}"'

View File

@ -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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View 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

View 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

View 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

View 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

View File

@ -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 %}

View File

@ -13,6 +13,7 @@
<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) }}">« {{ 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">

View File

@ -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 %}

View File

@ -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>

View File

@ -0,0 +1,466 @@
# Polish template for Calendar.social.
# Copyright (C) 2018 Marcin Mikołajczak
# This file is distributed under the same license as the Calendar.social project.
# Marcin Mikołajczak <me@m4sk.in>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.1\n"
"Report-Msgid-Bugs-To: gergely@polonkai.eu\n"
"POT-Creation-Date: 2018-07-30 22:05+0200\n"
"PO-Revision-Date: 2018-07-30 22:23+0200\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
"X-Generator: Poedit 2.0.9\n"
"Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"Language: pl\n"
#: calsocial/account.py:227
msgid "Cant invalidate your current session"
msgstr "Nie udało się unieważnić obecnej sesji"
#: calsocial/forms.py:57
msgid "This username is not available"
msgstr "Ta nazwa użytkownika nie jest dostepna"
#: calsocial/forms.py:88
msgid "This email address can not be used"
msgstr "Nie można użyć tego adresu e-mail"
#: calsocial/forms.py:100
msgid "Username"
msgstr "Nazwa użytkownika"
#: calsocial/forms.py:101
msgid "Email address"
msgstr "Adres e-mail"
#: calsocial/forms.py:102
msgid "Password"
msgstr "Hasło"
#: calsocial/forms.py:103
msgid "Password, once more"
msgstr "Potwórz hasło"
#: calsocial/forms.py:112
msgid "The two passwords must match!"
msgstr "Hasła muszą do siebie pasować!"
#: calsocial/forms.py:219
msgid "Title"
msgstr "Tytuł"
#: calsocial/forms.py:220 calsocial/forms.py:260
msgid "Time zone"
msgstr "Strefa czasowa"
#: calsocial/forms.py:221
msgid "Start time"
msgstr "Czas rozpoczęcia"
#: calsocial/forms.py:222
msgid "End time"
msgstr "Czas zakończenia"
#: calsocial/forms.py:223
msgid "All day"
msgstr "Cały dzień"
#: calsocial/forms.py:224
msgid "Description"
msgstr "Opis"
#: calsocial/forms.py:225
msgid "Visibility"
msgstr "Widoczność"
#: calsocial/forms.py:253
msgid "End time must be later than start time!"
msgstr "Czas zakończenia musi nastąpić po czasie rozpoczęcia!"
#: calsocial/forms.py:284
msgid "Username or email"
msgstr "Nazwa użytkownika lub adres e-mail"
#: calsocial/forms.py:372
msgid "User is already invited"
msgstr "Użytkownik został już zaproszony"
#: calsocial/forms.py:382 calsocial/forms.py:396
msgid "Display name"
msgstr "Nazwa wyświetlana"
#: calsocial/forms.py:385
msgid ""
"This will be shown to other users as your name. You can use your real name, "
"or any nickname you like."
msgstr ""
"Będzie widoczna dla innych użytkowników jako Twoja nazwa. Może to być Twoje "
"imię i nazwisko lub dowolny pseudonim."
#: calsocial/forms.py:387
msgid "Your time zone"
msgstr "Twoja strefa czasowa"
#: calsocial/forms.py:389
msgid "The start and end times of events will be displayed in this time zone."
msgstr ""
"Czas rozpoczęcia i zakończenia wydarzeń będą widoczne w tej strefie czasowej."
#: calsocial/forms.py:397
msgid "Use a built-in avatar"
msgstr "Użyj wbudowanego awatara"
#: calsocial/forms.py:398
msgid "Lock profile"
msgstr "Zablokuj konto"
#: calsocial/models.py:75
#, python-format
msgid "%(actor)s followed you"
msgstr "%(actor)s zaczął Cię śledzić"
#: calsocial/models.py:75
#, python-format
msgid "%(actor)s followed %(item)s"
msgstr "%(actor)s zaczął śledzić %(item)s"
#: calsocial/models.py:76
#, python-format
msgid "%(actor)s invited you to %(item)s"
msgstr "%(actor)s zaprosił Cię na %(item)s"
#: calsocial/models.py:121
msgid "Visible only to attendees"
msgstr "Widoczne tylko dla uczestników"
#: calsocial/models.py:122
msgid "Visible to everyone"
msgstr "Widoczne dla wszystkich"
#: calsocial/models.py:542
#, python-format
msgid "%(user)s logged in"
msgstr "%(user)s zalogował się"
#: calsocial/models.py:543
#, python-format
msgid "%(user)s failed to log in"
msgstr "nie udało się zalogować %(user)s"
#: calsocial/models.py:544
#, python-format
msgid "%(user)s logged out"
msgstr "%(user)s wylogował się"
#: calsocial/models.py:561
#, python-format
msgid "UNKNOWN RECORD \"%(log_type)s\" for %(user)s"
msgstr "NIEZNANY WPIS \"%(log_type)s\" dla %(user)s"
#: calsocial/calendar_system/gregorian.py:31
msgid "Gregorian"
msgstr "Gregoriański"
#: calsocial/calendar_system/gregorian.py:39
msgid "January"
msgstr "Styczeń"
#: calsocial/calendar_system/gregorian.py:40
msgid "February"
msgstr "Luty"
#: calsocial/calendar_system/gregorian.py:41
msgid "March"
msgstr "Marzec"
#: calsocial/calendar_system/gregorian.py:42
msgid "April"
msgstr "Kwiecień"
#: calsocial/calendar_system/gregorian.py:43
msgid "May"
msgstr "Maj"
#: calsocial/calendar_system/gregorian.py:44
msgid "June"
msgstr "Czerwiec"
#: calsocial/calendar_system/gregorian.py:45
msgid "July"
msgstr "Lipiec"
#: calsocial/calendar_system/gregorian.py:46
msgid "August"
msgstr "Sierpień"
#: calsocial/calendar_system/gregorian.py:47
msgid "September"
msgstr "Wrzesień"
#: calsocial/calendar_system/gregorian.py:48
msgid "October"
msgstr "Październik"
#: calsocial/calendar_system/gregorian.py:49
msgid "November"
msgstr "Listopad"
#: calsocial/calendar_system/gregorian.py:50
msgid "December"
msgstr "Grudzień"
#: calsocial/calendar_system/gregorian.py:54
msgid "Monday"
msgstr "Poniedziałek"
#: calsocial/calendar_system/gregorian.py:55
msgid "Tuesday"
msgstr "Wtorek"
#: calsocial/calendar_system/gregorian.py:56
msgid "Wednesday"
msgstr "Środa"
#: calsocial/calendar_system/gregorian.py:57
msgid "Thursday"
msgstr "Czwartek"
#: calsocial/calendar_system/gregorian.py:58
msgid "Friday"
msgstr "Piątek"
#: calsocial/calendar_system/gregorian.py:59
msgid "Saturday"
msgstr "Sobota"
#: calsocial/calendar_system/gregorian.py:60
msgid "Sunday"
msgstr "Niedziela"
#: calsocial/templates/base.html:29 calsocial/templates/login.html:9
#: calsocial/templates/login.html:24 calsocial/templates/welcome.html:9
#: calsocial/templates/welcome.html:16
msgid "Login"
msgstr "Zaloguj się"
#: calsocial/templates/base.html:32
#, python-format
msgid "Logged in as %(username)s"
msgstr "Zalogowano jako %(username)s"
#: calsocial/templates/base.html:36
msgid "Calendar view"
msgstr "Widok kalendarza"
#: calsocial/templates/base.html:37
msgid "Notifications"
msgstr "Powiadomienia"
#: calsocial/templates/account/settings-base.html:8
#: calsocial/templates/account/user-settings.html:5
#: calsocial/templates/base.html:38
msgid "Settings"
msgstr "Ustawienia"
#: calsocial/templates/base.html:39
msgid "Logout"
msgstr "Wyloguj się"
#: calsocial/templates/base.html:48
msgid "About this instance"
msgstr "O tej instancji"
#: calsocial/templates/event-details.html:5
#, python-format
msgid ""
"This event is organised in the %(timezone)s time zone, in which it happens "
"between %(start_time)s and %(end_time)s"
msgstr ""
"To wydarzenie jest organizowane w strefie czasowej %(timezone)s, w której "
"wydarzy się ono pomiędzy %(start_time)s a %(end_time)s"
#: calsocial/templates/event-details.html:29
msgid "Invited users"
msgstr "Zaproszeni użytkownicy"
#: calsocial/templates/event-details.html:41
#: calsocial/templates/event-details.html:47
msgid "Invite"
msgstr "Zaproś"
#: calsocial/templates/account/first-steps.html:25
#: calsocial/templates/account/profile-edit.html:20
#: calsocial/templates/account/user-settings.html:18
#: calsocial/templates/event-edit.html:24
msgid "Save"
msgstr "Zapisz"
#: calsocial/templates/index.html:4
#, python-format
msgid "Welcome to Calendar.social, %(username)s!"
msgstr "Witaj na Calendar.social, %(username)s!"
#: calsocial/templates/index.html:10
msgid "Add event"
msgstr "Dodaj wydarzenie"
#: calsocial/templates/profile-details.html:10
#: calsocial/templates/profile-details.html:11
msgid "locked profile"
msgstr "profil zablokowany"
#: calsocial/templates/profile-details.html:17
msgid "Follow"
msgstr "Obserwuj"
#: calsocial/templates/profile-details.html:21
msgid "Follows"
msgstr "Obserwacje"
#: calsocial/templates/profile-details.html:29
msgid "Followers"
msgstr "Obserwujący"
#: calsocial/templates/welcome.html:20
msgid "Or"
msgstr "Lub"
#: calsocial/templates/welcome.html:22
msgid "Register an account"
msgstr "Zarejestruj się"
#: calsocial/templates/welcome.html:26
msgid "What is Calendar.social?"
msgstr "Czym jest Calendar.social?"
#: calsocial/templates/welcome.html:28
msgid ""
"Calendar.social is a calendar app based on open protocols and free, open "
"source software."
msgstr ""
"Calendar.social jest aplikacją kalendarza opartą na otwartych protokołach i "
"wolnym, otwartoźródłowym oprogramowaniu."
#: calsocial/templates/welcome.html:29
msgid "It is decentralised like one of its counterparts, email."
msgstr ""
"Jest zdecentralizowana tak, jak jak jeden z jej odpowiedników e-mail."
#: calsocial/templates/welcome.html:33
msgid "Go to your calendar"
msgstr "Przejdź do swojego kalendarza"
#: calsocial/templates/welcome.html:37
msgid "Peek inside"
msgstr "Zajrzyj do środka"
#: calsocial/templates/welcome.html:43
msgid "Built with users in mind"
msgstr "Stworzony z myślą o użytkownikach"
#: calsocial/templates/welcome.html:45
msgid ""
"From planning your appointments to organising large scale events Calendar."
"social can help with all your scheduling needs."
msgstr ""
"Calendar.social może pomóc w każdej potrzebie związanej z planowaniem, od "
"planowania spotkań do organizowania wydarzeń na większą skalę."
#: calsocial/templates/welcome.html:54
msgid "user"
msgid_plural "users"
msgstr[0] "użytkownik"
msgstr[1] "użytkowników"
msgstr[2] "użytkowników"
#: calsocial/templates/welcome.html:60
msgid "event"
msgid_plural "events"
msgstr[0] "wydarzenia"
msgstr[1] "wydarzenia"
msgstr[2] "wydarzeń"
#: calsocial/templates/welcome.html:67
msgid "Built for people"
msgstr "Zbudowany dla ludzi"
#: calsocial/templates/welcome.html:69
msgid "Calendar.social is not a commercial network."
msgstr "Calendar.social nie jest komercyjną siecią."
#: calsocial/templates/welcome.html:70
msgid "No advertising, no data mining, no walled gardens."
msgstr "Brak reklam, zbierania danych i ograniczeń."
#: calsocial/templates/welcome.html:71
msgid "There is no central authority."
msgstr "Brak centralnej władzy."
#: calsocial/templates/welcome.html:77
msgid "Administered by"
msgstr "Administrowane przez"
#: calsocial/templates/account/active-sessions.html:4
#: calsocial/templates/account/settings-base.html:9
msgid "Active sessions"
msgstr "Aktywne sesje"
#: calsocial/templates/account/active-sessions.html:10
msgid "Invalidate"
msgstr "Unieważnij"
#: calsocial/templates/account/first-steps.html:5
msgid "First steps"
msgstr "Pierwsze kroki"
#: calsocial/templates/account/first-steps.html:7
msgid "Welcome to Calendar.social!"
msgstr "Witamy na Calendar.social!"
#: calsocial/templates/account/first-steps.html:10
msgid ""
"These are the first steps you should make before you can start using the "
"site."
msgstr ""
"Oto pierwsze kroki, które powinieneś wykonać, zanim zaczniesz używać tej "
"strony."
#: calsocial/templates/account/follow-requests.html:4
msgid "Follow requests"
msgstr "Prośby o możliwość obserwacji"
#: calsocial/templates/account/follow-requests.html:10
msgid "Accept"
msgstr "Zaakceptuj"
#: calsocial/templates/account/follow-requests.html:15
msgid "No requests to display."
msgstr "Brak próśb do wyświetlenia."
#: calsocial/templates/account/follow-requests.html:19
msgid "Your profile is not locked."
msgstr "Twój profil nie jest zablokowany."
#: calsocial/templates/account/follow-requests.html:20
msgid "Anyone can follow you without your consent."
msgstr "Każdy może Cię zaobserwować bez Twojego zezwolenia."
#: calsocial/templates/account/notifications.html:7
msgid "Nothing to show."
msgstr "Nie ma nic do pokazania."
#: calsocial/templates/account/profile-edit.html:5
#: calsocial/templates/account/settings-base.html:7
msgid "Edit profile"
msgstr "Edytuj profil"
#: calsocial/templates/account/registration.html:17
msgid "Register"
msgstr "Zarejestruj się"

View File

@ -89,6 +89,9 @@ 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)

62
tests/conftest.py Normal file
View 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()

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 and fixtures for testing
"""Helper functions for testing
"""
import pytest
from contextlib import contextmanager
import calsocial
from calsocial.models import db
@ -32,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
"""
@ -57,16 +41,20 @@ 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
"""
@contextmanager
def alter_config(app, **kwargs):
saved = {}
configure_app()
for key, value in kwargs.items():
if key in app.config:
saved[key] = app.config[key]
with calsocial.app.app_context():
db.create_all()
app.config[key] = value
yield db
yield
db.drop_all()
for key, value in kwargs.items():
if key in saved:
app.config[key] = saved[key]
else:
del app.config[key]

49
tests/test_app_state.py Normal file
View 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'

View File

@ -17,12 +17,60 @@
"""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
"""
page = client.get('/')
assert b'Welcome to Calendar.social' in page.data
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

View File

@ -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
View 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'

View File

@ -20,15 +20,7 @@
import calsocial
from calsocial.models import db, User
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
from helpers import login
def test_login_invalid_user(client):
@ -81,4 +73,4 @@ def test_login_first_steps(client):
assert page.location == 'http://localhost/'
page = client.get('/')
assert page.location == 'http://localhost/first-steps'
assert page.location == 'http://localhost/accounts/first-steps'

View File

@ -20,28 +20,28 @@
import calsocial
from calsocial.models import db, User
from helpers import client
from helpers import alter_config
def test_register_page(client):
"""Test the registration page
"""
page = client.get('/register')
page = client.get('/accounts/register')
assert b'Register</button>' in page.data
def test_register_post_empty(client):
"""Test sending empty registration data
"""
page = client.post('/register', data={})
page = client.post('/accounts/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('/register', data={
page = client.post('/accounts/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('/register', data={
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
@ -65,13 +65,12 @@ def test_register(client):
"""Test user registration
"""
page = client.post('/register', data={
page = client.post('/accounts/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/'
@ -90,7 +89,7 @@ def test_register_existing_username(client):
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
@ -107,10 +106,16 @@ def test_register_existing_email(client):
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
page = client.post('/accounts/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