Compare commits
	
		
			1 Commits
		
	
	
		
			response-v
			...
			vagrant
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 60ad2c7ae2 | 
| @@ -1 +0,0 @@ | |||||||
| FLASK_ENV=testing |  | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,5 +4,5 @@ __pycache__/ | |||||||
| /calsocial/translations/*/LC_MESSAGES/*.mo | /calsocial/translations/*/LC_MESSAGES/*.mo | ||||||
| /.pytest_cache/ | /.pytest_cache/ | ||||||
| /.env | /.env | ||||||
| /.coverage | /.vagrant/ | ||||||
| /htmlcov/ | /ansible/*.retry | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -18,7 +18,6 @@ flask-caching = "*" | |||||||
| [dev-packages] | [dev-packages] | ||||||
| pylint = "*" | pylint = "*" | ||||||
| pytest = "*" | pytest = "*" | ||||||
| pytest-cov = "*" |  | ||||||
|  |  | ||||||
| [requires] | [requires] | ||||||
| python_version = "3.6" | python_version = "3.6" | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										66
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294" |             "sha256": "e4313bc9baef5cb187176951d45094fe1de4ccba0d15ab58efbac21b6434f255" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @@ -284,10 +284,10 @@ | |||||||
|     "develop": { |     "develop": { | ||||||
|         "astroid": { |         "astroid": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", |                 "sha256:8704779744963d56a2625ec2949eb150bd499fc099510161ddbb2b64e2d98138", | ||||||
|                 "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" |                 "sha256:add3fd690e7c1fe92436d17be461feeaa173e6f33e0789734310334da0f30027" | ||||||
|             ], |             ], | ||||||
|             "version": "==2.0.1" |             "version": "==2.0" | ||||||
|         }, |         }, | ||||||
|         "atomicwrites": { |         "atomicwrites": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -303,50 +303,6 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==18.1.0" |             "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": { |         "isort": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", |                 "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", | ||||||
| @@ -421,11 +377,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", |                 "sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4", | ||||||
|                 "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" |                 "sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.0.1" |             "version": "==2.0.0" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -435,14 +391,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.6.3" |             "version": "==3.6.3" | ||||||
|         }, |         }, | ||||||
|         "pytest-cov": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", |  | ||||||
|                 "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" |  | ||||||
|             ], |  | ||||||
|             "index": "pypi", |  | ||||||
|             "version": "==2.5.1" |  | ||||||
|         }, |  | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", |                 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										23
									
								
								ansible/dev.yml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										12
									
								
								ansible/install.sh
									
									
									
									
									
										Normal 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." | ||||||
							
								
								
									
										8
									
								
								ansible/roles/common/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ansible/roles/common/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | --- | ||||||
|  |  | ||||||
|  | - name: Install required packages | ||||||
|  |   dnf: | ||||||
|  |     name: "{{ item }}" | ||||||
|  |     state: present | ||||||
|  |   with_items: | ||||||
|  |     - libselinux-python | ||||||
							
								
								
									
										10
									
								
								ansible/roles/common/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								ansible/roles/common/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | --- | ||||||
|  |  | ||||||
|  | # Project name | ||||||
|  | project_name: calendar.social | ||||||
|  |  | ||||||
|  | # Project path | ||||||
|  | project_path: /vagrant | ||||||
|  |  | ||||||
|  | # Flask app path | ||||||
|  | application_path: /vagrant/app | ||||||
							
								
								
									
										56
									
								
								ansible/roles/gunicorn/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								ansible/roles/gunicorn/tasks/main.yml
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										9
									
								
								ansible/roles/gunicorn/templates/gunicorn.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ansible/roles/gunicorn/templates/gunicorn.conf.j2
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										8
									
								
								ansible/roles/gunicorn/templates/supervisor.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ansible/roles/gunicorn/templates/supervisor.conf.j2
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										11
									
								
								ansible/roles/nginx/handlers/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ansible/roles/nginx/handlers/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | --- | ||||||
|  |  | ||||||
|  | - name: Reload Nginx | ||||||
|  |   service: | ||||||
|  |     name: nginx | ||||||
|  |     state: reloaded | ||||||
|  |  | ||||||
|  | - name: Stop Nginx | ||||||
|  |   service: | ||||||
|  |     name: nginx | ||||||
|  |     state: stopped | ||||||
							
								
								
									
										42
									
								
								ansible/roles/nginx/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								ansible/roles/nginx/tasks/main.yml
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										41
									
								
								ansible/roles/nginx/templates/site-ssl.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								ansible/roles/nginx/templates/site-ssl.conf.j2
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								ansible/roles/nginx/templates/site.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ansible/roles/nginx/templates/site.conf.j2
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								ansible/roles/nginx/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ansible/roles/nginx/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | --- | ||||||
|  |  | ||||||
|  | host_name: calendar-social.local | ||||||
							
								
								
									
										22
									
								
								ansible/roles/python/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ansible/roles/python/tasks/main.yml
									
									
									
									
									
										Normal 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 }}" | ||||||
| @@ -19,10 +19,8 @@ | |||||||
|  |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| import os | import os | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \ | from flask import Flask, abort, current_app, redirect, render_template, request, url_for | ||||||
|     url_for |  | ||||||
| from flask_babelex import Babel, get_locale as babel_get_locale | from flask_babelex import Babel, get_locale as babel_get_locale | ||||||
| from flask_security import SQLAlchemyUserDatastore, current_user, login_required | from flask_security import SQLAlchemyUserDatastore, current_user, login_required | ||||||
| from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound | ||||||
| @@ -74,22 +72,12 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         self._timezone = None |         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) |         self.config.from_pyfile(f'config_{config_name}.py', True) | ||||||
|         # Make sure we look up users both by their usernames and email addresses |         # Make sure we look up users both by their usernames and email addresses | ||||||
|         self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') |         self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') | ||||||
|         self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html' |         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 |         self.jinja_env.policies['ext.i18n.trimmed'] = True  # pylint: disable=no-member | ||||||
|  |  | ||||||
|         db.init_app(self) |         db.init_app(self) | ||||||
| @@ -119,7 +107,7 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         if current_user.is_authenticated and \ |         if current_user.is_authenticated and \ | ||||||
|            not current_user.profile and \ |            not current_user.profile and \ | ||||||
|            request.endpoint != 'account.first_steps': |            request.endpoint != 'first_steps': | ||||||
|             return redirect(url_for('account.first_steps')) |             return redirect(url_for('account.first_steps')) | ||||||
|  |  | ||||||
|         return None |         return None | ||||||
| @@ -129,6 +117,9 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         """The default time zone of the app |         """The default time zone of the app | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         from warnings import warn | ||||||
|  |  | ||||||
|  |         from flask import has_app_context | ||||||
|         from pytz import timezone, utc |         from pytz import timezone, utc | ||||||
|         from pytz.exceptions import UnknownTimeZoneError |         from pytz.exceptions import UnknownTimeZoneError | ||||||
|  |  | ||||||
| @@ -147,32 +138,6 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         return self._timezone |         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 |     @staticmethod | ||||||
|     def _current_calendar(): |     def _current_calendar(): | ||||||
|         from .calendar_system.gregorian import GregorianCalendar |         from .calendar_system.gregorian import GregorianCalendar | ||||||
| @@ -201,16 +166,13 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         user_count = User.query.count() |         user_count = User.query.count() | ||||||
|         event_count = Event.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', |         return render_template('welcome.html', | ||||||
|                                calendar=calendar, |                                calendar=calendar, | ||||||
|                                user_only=False, |                                user_only=False, | ||||||
|                                login_form=login_form, |                                login_form=login_form, | ||||||
|                                user_count=user_count, |                                user_count=user_count, | ||||||
|                                event_count=event_count, |                                event_count=event_count) | ||||||
|                                admin_profile=admin_profile) |  | ||||||
|  |  | ||||||
|     @RoutedMixin.route('/') |     @RoutedMixin.route('/') | ||||||
|     def hello(self): |     def hello(self): | ||||||
|   | |||||||
| @@ -6,4 +6,4 @@ from calsocial import CalendarSocialApp | |||||||
|  |  | ||||||
| app = CalendarSocialApp('calsocial') | app = CalendarSocialApp('calsocial') | ||||||
|  |  | ||||||
| app.run() | app.run(host='0.0.0.0', port=80) | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ class AccountBlueprint(Blueprint, RoutedMixin): | |||||||
|         app.register_blueprint(self, url_prefix=url_prefix) |         app.register_blueprint(self, url_prefix=url_prefix) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/register', methods=['POST', 'GET']) |     @RoutedMixin.route('/register') | ||||||
|     def register_account(): |     def register_account(): | ||||||
|         """View for user registration |         """View for user registration | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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}) |  | ||||||
| @@ -21,7 +21,7 @@ from datetime import timedelta | |||||||
| import pickle | import pickle | ||||||
| from uuid import uuid4 | 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.sessions import SessionInterface, SessionMixin | ||||||
| from flask_caching import Cache | from flask_caching import Cache | ||||||
| from werkzeug.datastructures import CallbackDict | from werkzeug.datastructures import CallbackDict | ||||||
| @@ -46,7 +46,7 @@ class CachedSession(CallbackDict, SessionMixin):  # pylint: disable=too-many-anc | |||||||
|             self.__modifying = True |             self.__modifying = True | ||||||
|  |  | ||||||
|             if has_request_context(): |             if has_request_context(): | ||||||
|                 self['ip'] = flask_request.remote_addr |                 self['ip'] = request.remote_addr | ||||||
|  |  | ||||||
|             self.modified = True |             self.modified = True | ||||||
|  |  | ||||||
| @@ -59,9 +59,6 @@ class CachedSession(CallbackDict, SessionMixin):  # pylint: disable=too-many-anc | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def user(self): |     def user(self): | ||||||
|         """The user this session belongs to |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from calsocial.models import User |         from calsocial.models import User | ||||||
|  |  | ||||||
|         if 'user_id' not in self: |         if 'user_id' not in self: | ||||||
| @@ -90,11 +87,11 @@ class CachedSessionInterface(SessionInterface): | |||||||
|         return str(uuid4()) |         return str(uuid4()) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get_cache_expiration_time(app, sess): |     def get_cache_expiration_time(app, session): | ||||||
|         """Get the expiration time of the cache entry |         """Get the expiration time of the cache entry | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if sess.permanent: |         if session.permanent: | ||||||
|             return app.permanent_session_lifetime |             return app.permanent_session_lifetime | ||||||
|  |  | ||||||
|         return timedelta(days=1) |         return timedelta(days=1) | ||||||
| @@ -150,10 +147,7 @@ class CachedSessionInterface(SessionInterface): | |||||||
|                             domain=domain) |                             domain=domain) | ||||||
|  |  | ||||||
|     def delete_session(self, sid): |     def delete_session(self, sid): | ||||||
|         """Delete the session with ``sid`` as its session ID |         if has_request_context() and session.sid == sid: | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if has_request_context() and flask_session.sid == sid: |  | ||||||
|             raise ValueError('Will not delete the current session') |             raise ValueError('Will not delete the current session') | ||||||
|  |  | ||||||
|         cache.delete(self.prefix + sid) |         cache.delete(self.prefix + sid) | ||||||
|   | |||||||
| @@ -18,12 +18,24 @@ | |||||||
| """ | """ | ||||||
|  |  | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
|  | from functools import wraps | ||||||
|  |  | ||||||
| from flask_babelex import lazy_gettext as _ | from flask_babelex import lazy_gettext as _ | ||||||
|  |  | ||||||
| from . import CalendarSystem | 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): | class GregorianCalendar(CalendarSystem): | ||||||
|     """Gregorian calendar system for Calendar.social |     """Gregorian calendar system for Calendar.social | ||||||
|     """ |     """ | ||||||
| @@ -92,6 +104,7 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         return day_list |         return day_list | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|  |     @to_timestamp | ||||||
|     def prev_year(self): |     def prev_year(self): | ||||||
|         """Returns the timestamp of the same date in the previous year |         """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 |         return self.timestamp.replace(year=self.timestamp.year - 1).year | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|  |     @to_timestamp | ||||||
|     def prev_month(self): |     def prev_month(self): | ||||||
|         """Returns the timestamp of the same day in the previous month |         """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] |         return self.month_names[timestamp.month - 1] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|  |     @to_timestamp | ||||||
|     def next_month(self): |     def next_month(self): | ||||||
|         """Returns the timestamp of the same day in the next month |         """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] |         return self.month_names[timestamp.month - 1] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|  |     @to_timestamp | ||||||
|     def next_year(self): |     def next_year(self): | ||||||
|         """Returns the timestamp of the same date in the next year |         """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) |         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 |     @staticmethod | ||||||
|     def day_events(date, user=None): |     def day_events(date, user=None): | ||||||
| @@ -203,7 +219,7 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         end_timestamp = start_timestamp + timedelta(days=1) |         end_timestamp = start_timestamp + timedelta(days=1) | ||||||
|  |  | ||||||
|         events = events.filter((Event.start_time <= end_timestamp) & |         events = events.filter((Event.start_time <= end_timestamp) & | ||||||
|                                (Event.end_time >= start_timestamp)) \ |                                 (Event.end_time >= start_timestamp)) \ | ||||||
|                        .order_by('start_time', 'end_time') |                        .order_by('start_time', 'end_time') | ||||||
|  |  | ||||||
|         if user is None: |         if user is None: | ||||||
|   | |||||||
| @@ -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' |  | ||||||
| @@ -23,7 +23,7 @@ from flask_babelex import lazy_gettext as _ | |||||||
| from flask_security.forms import LoginForm as BaseLoginForm | from flask_security.forms import LoginForm as BaseLoginForm | ||||||
| from flask_wtf import FlaskForm | from flask_wtf import FlaskForm | ||||||
| import pytz | 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.ext.dateutil.fields import DateTimeField | ||||||
| from wtforms.validators import DataRequired, Email, StopValidation, ValidationError | from wtforms.validators import DataRequired, Email, StopValidation, ValidationError | ||||||
| from wtforms.widgets import TextArea | from wtforms.widgets import TextArea | ||||||
| @@ -394,19 +394,11 @@ class ProfileForm(FlaskForm): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     display_name = StringField(label=_('Display name'), validators=[DataRequired()]) |     display_name = StringField(label=_('Display name'), validators=[DataRequired()]) | ||||||
|     builtin_avatar = RadioField(label=_('Use a built-in avatar')) |  | ||||||
|     locked = BooleanField(label=_('Lock profile')) |     locked = BooleanField(label=_('Lock profile')) | ||||||
|  |  | ||||||
|     def __init__(self, profile, *args, **kwargs): |     def __init__(self, profile, *args, **kwargs): | ||||||
|         from flask import current_app |         kwargs.update({'display_name': profile.display_name}) | ||||||
|  |         kwargs.update({'locked': profile.locked}) | ||||||
|         kwargs.update( |  | ||||||
|             { |  | ||||||
|                 'display_name': profile.display_name, |  | ||||||
|                 'locked': profile.locked, |  | ||||||
|                 'builtin_avatar': profile.builtin_avatar, |  | ||||||
|             }) |  | ||||||
|         FlaskForm.__init__(self, *args, **kwargs) |         FlaskForm.__init__(self, *args, **kwargs) | ||||||
|  |  | ||||||
|         self.builtin_avatar.choices = [(name, name) for name in current_app.config['BUILTIN_AVATARS']] |  | ||||||
|         self.profile = profile |         self.profile = profile | ||||||
|   | |||||||
| @@ -21,14 +21,12 @@ from datetime import datetime | |||||||
| from enum import Enum | from enum import Enum | ||||||
| from warnings import warn | from warnings import warn | ||||||
|  |  | ||||||
| from flask import current_app |  | ||||||
| from flask_babelex import lazy_gettext | from flask_babelex import lazy_gettext | ||||||
| from flask_security import UserMixin, RoleMixin | from flask_security import UserMixin, RoleMixin | ||||||
| from flask_sqlalchemy import SQLAlchemy | from flask_sqlalchemy import SQLAlchemy | ||||||
| from sqlalchemy.orm.exc import NoResultFound | from sqlalchemy.orm.exc import NoResultFound | ||||||
| from sqlalchemy_utils.types.choice import ChoiceType | from sqlalchemy_utils.types.choice import ChoiceType | ||||||
|  |  | ||||||
| from .app_state import app_state_base |  | ||||||
| from .cache import cache | from .cache import cache | ||||||
| from .utils import force_locale | from .utils import force_locale | ||||||
|  |  | ||||||
| @@ -70,18 +68,6 @@ class NotificationAction(Enum): | |||||||
|     #: A user has been invited to an event |     #: A user has been invited to an event | ||||||
|     invite = 2 |     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 = { | NOTIFICATION_ACTION_MESSAGES = { | ||||||
|     NotificationAction.follow: (_('%(actor)s followed you'), _('%(actor)s followed %(item)s')), |     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 invitee’s friends (ie. mutual follows) |  | ||||||
|     friends = 3 |  | ||||||
|  |  | ||||||
|     #: The response is visible to the invitee’s 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: | class SettingsProxy: | ||||||
|     """Proxy object to get settings for a user |     """Proxy object to get settings for a user | ||||||
|     """ |     """ | ||||||
| @@ -294,6 +206,7 @@ class User(db.Model, UserMixin): | |||||||
|         If the user didn’t set a time zone yet, the application default is used. |         If the user didn’t set a time zone yet, the application default is used. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         from flask import current_app | ||||||
|         from pytz import timezone |         from pytz import timezone | ||||||
|         from pytz.exceptions import UnknownTimeZoneError |         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 owner’s consent |     #: If locked, a profile cannot be followed without the owner’s consent | ||||||
|     locked = db.Column(db.Boolean(), default=False) |     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 |     @property | ||||||
|     def fqn(self): |     def fqn(self): | ||||||
|         """The fully qualified name of the profile |         """The fully qualified name of the profile | ||||||
| @@ -490,45 +400,6 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
|         return notification |         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): | class Event(db.Model): | ||||||
|     """Database model for events |     """Database model for events | ||||||
| @@ -915,112 +786,3 @@ class Response(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
|     #: The response itself |     #: The response itself | ||||||
|     response = db.Column(db.Enum(ResponseType), nullable=False) |     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}"' |  | ||||||
|   | |||||||
| @@ -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 | 
| @@ -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 | 
| @@ -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 | 
| @@ -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 | 
| @@ -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 | 
| @@ -18,13 +18,3 @@ | |||||||
|     {% endif %} |     {% endif %} | ||||||
| </div> | </div> | ||||||
| {% endmacro %} | {% endmacro %} | ||||||
|  |  | ||||||
| {% macro profile_link(profile) %} |  | ||||||
| <a href="{% if profile %}{{ url_for('display_profile', username=profile.user.username) }}{% else %}#{% endif %}" class="ui profile"> |  | ||||||
|     {% if profile and profile.builtin_avatar %} |  | ||||||
|     <img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular avatar image"> |  | ||||||
|     {% endif %} |  | ||||||
|     <div class="display name">{{ profile.display_name }}</div> |  | ||||||
|     <div class="handle">{{ profile or '' }}</div> |  | ||||||
| </a> |  | ||||||
| {% endmacro %} |  | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ | |||||||
|     <br> |     <br> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|     {{ field(form.builtin_avatar) }} |  | ||||||
|     {{ field(form.display_name) }} |     {{ field(form.display_name) }} | ||||||
|     {{ field(form.locked, inline=true) }} |     {{ field(form.locked, inline=true) }} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,10 +11,10 @@ | |||||||
|         </tr> |         </tr> | ||||||
|         <tr class="month"> |         <tr class="month"> | ||||||
|             <td> |             <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> | ||||||
|             <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> | ||||||
|             <td colspan="3" class="month-name"> |             <td colspan="3" class="month-name"> | ||||||
| {% if not calendar.has_today %} | {% if not calendar.has_today %} | ||||||
| @@ -26,10 +26,10 @@ | |||||||
| {% endif %} | {% endif %} | ||||||
|             </td> |             </td> | ||||||
|             <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> | ||||||
|             <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> |             </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr class="days"> |         <tr class="days"> | ||||||
|   | |||||||
| @@ -1,18 +1,14 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| {% from '_macros.html' import profile_link %} |  | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h2 class="ui header"> | <h1> | ||||||
|     {% if profile.builtin_avatar %} |  | ||||||
|     <img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular image"> |  | ||||||
|     {% endif %} |  | ||||||
|     {% if profile.locked %} |     {% if profile.locked %} | ||||||
|     <i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i> |     <i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i> | ||||||
|     <span class="sr-only">{% trans %}locked profile{% endtrans %}</span> |     <span class="sr-only">{% trans %}locked profile{% endtrans %}</span> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {{ profile.display_name }} |     {{ profile.display_name }} | ||||||
|     <small>@{{ profile.user.username}}</small> |     <small>@{{ profile.user.username}}</small> | ||||||
| </h2> | </h1> | ||||||
|     {% if profile.user != current_user %} |     {% if profile.user != current_user %} | ||||||
| <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> | <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| @@ -22,7 +18,7 @@ | |||||||
| </h2> | </h2> | ||||||
|  |  | ||||||
|     {% for followed in profile.followed_list %} |     {% for followed in profile.followed_list %} | ||||||
| {{ profile_link(followed) }} | {{ followed }} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|  |  | ||||||
| <h2> | <h2> | ||||||
| @@ -30,6 +26,6 @@ | |||||||
| </h2> | </h2> | ||||||
|  |  | ||||||
|     {% for follower in profile.follower_list %} |     {% for follower in profile.follower_list %} | ||||||
| {{ profile_link(follower) }} | {{ follower }} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| {% from '_macros.html' import profile_link %} |  | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="ui grid"> | <div class="ui grid"> | ||||||
| @@ -73,10 +72,12 @@ | |||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div class="four wide column"> |         <div class="four wide column"> | ||||||
|     {% if admin_profile %} |  | ||||||
|             <h2>{% trans %}Administered by{% endtrans %}</h2> |             <h2>{% trans %}Administered by{% endtrans %}</h2> | ||||||
|             {{ profile_link(admin_profile) }} |             <a href="#" class="ui profile"> | ||||||
|     {% endif %} |                 <div class="avatar"></div> | ||||||
|  |                 <div class="display name">Your Admin here</div> | ||||||
|  |                 <div class="handle">@admin@he.re</div> | ||||||
|  |             </a> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -89,9 +89,6 @@ class RoutedMixin: | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def register_routes(self): |     def register_routes(self): | ||||||
|         """Register all routes that were marked with :meth:`route` |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         for attr_name in self.__dir__(): |         for attr_name in self.__dir__(): | ||||||
|             attr = getattr(self, attr_name) |             attr = getattr(self, attr_name) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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() |  | ||||||
| @@ -14,10 +14,10 @@ | |||||||
| # You should have received a copy of the GNU Affero General Public License | # 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/>. | # 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 | import calsocial | ||||||
| from calsocial.models import db | from calsocial.models import db | ||||||
| @@ -32,6 +32,22 @@ def configure_app(): | |||||||
|     calsocial.app.config['WTF_CSRF_ENABLED'] = False |     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): | def login(client, username, password, no_redirect=False): | ||||||
|     """Login with the specified username and password |     """Login with the specified username and password | ||||||
|     """ |     """ | ||||||
| @@ -41,20 +57,16 @@ def login(client, username, password, no_redirect=False): | |||||||
|                        follow_redirects=not no_redirect) |                        follow_redirects=not no_redirect) | ||||||
|  |  | ||||||
|  |  | ||||||
| @contextmanager | @pytest.fixture | ||||||
| def alter_config(app, **kwargs): | def database(): | ||||||
|     saved = {} |     """Fixture to provide all database tables in an active application context | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     for key, value in kwargs.items(): |     configure_app() | ||||||
|         if key in app.config: |  | ||||||
|             saved[key] = app.config[key] |  | ||||||
|  |  | ||||||
|         app.config[key] = value |     with calsocial.app.app_context(): | ||||||
|  |         db.create_all() | ||||||
|  |  | ||||||
|     yield |         yield db | ||||||
|  |  | ||||||
|     for key, value in kwargs.items(): |         db.drop_all() | ||||||
|         if key in saved: |  | ||||||
|             app.config[key] = saved[key] |  | ||||||
|         else: |  | ||||||
|             del app.config[key] |  | ||||||
|   | |||||||
| @@ -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' |  | ||||||
| @@ -17,60 +17,12 @@ | |||||||
| """General tests for Calendar.social | """General tests for Calendar.social | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from flask import current_app | from helpers import client | ||||||
|  |  | ||||||
| import pytest |  | ||||||
|  |  | ||||||
| def test_index_no_login(client): | def test_index_no_login(client): | ||||||
|     """Test the main page without logging in |     """Test the main page without logging in | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.get('/') |     page = client.get('/') | ||||||
|     assert b'Peek inside' in page.data |     assert b'Welcome to Calendar.social' in page.data | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_instance_adin_unset(database): |  | ||||||
|     """Test the instance admin feature if the admin is not set |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'): |  | ||||||
|         assert current_app.instance_admin is None |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_instance_admin_bad_value(database): |  | ||||||
|     """Test the instance admin feature if the value is invalid |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     from calsocial.models import AppState |  | ||||||
|  |  | ||||||
|     AppState['instance_admin'] = 'value' |  | ||||||
|  |  | ||||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'): |  | ||||||
|         assert current_app.instance_admin is None |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_instance_admin_doesnot_exist(database): |  | ||||||
|     """Test the instance admin feature if the admin user does not exist |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     from calsocial.models import AppState |  | ||||||
|  |  | ||||||
|     AppState['instance_admin'] = '0' |  | ||||||
|  |  | ||||||
|     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'): |  | ||||||
|         assert current_app.instance_admin is None |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_instance_admin(database): |  | ||||||
|     """Test the instance admin feature if the admin user does not exist |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     from calsocial.models import db, AppState, User |  | ||||||
|  |  | ||||||
|     user = User(username='admin') |  | ||||||
|     db.session.add(user) |  | ||||||
|     db.session.commit() |  | ||||||
|  |  | ||||||
|     AppState['instance_admin'] = user.id |  | ||||||
|  |  | ||||||
|     assert current_app.instance_admin == user |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
| import calsocial | import calsocial | ||||||
| from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow | 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): | def test_profile_follow(database): | ||||||
|   | |||||||
| @@ -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' |  | ||||||
| @@ -20,7 +20,15 @@ | |||||||
| import calsocial | import calsocial | ||||||
| from calsocial.models import db, User | 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): | def test_login_invalid_user(client): | ||||||
| @@ -73,4 +81,4 @@ def test_login_first_steps(client): | |||||||
|     assert page.location == 'http://localhost/' |     assert page.location == 'http://localhost/' | ||||||
|  |  | ||||||
|     page = client.get('/') |     page = client.get('/') | ||||||
|     assert page.location == 'http://localhost/accounts/first-steps' |     assert page.location == 'http://localhost/first-steps' | ||||||
|   | |||||||
| @@ -20,28 +20,28 @@ | |||||||
| import calsocial | import calsocial | ||||||
| from calsocial.models import db, User | from calsocial.models import db, User | ||||||
|  |  | ||||||
| from helpers import alter_config | from helpers import client | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_register_page(client): | def test_register_page(client): | ||||||
|     """Test the registration page |     """Test the registration page | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.get('/accounts/register') |     page = client.get('/register') | ||||||
|     assert b'Register</button>' in page.data |     assert b'Register</button>' in page.data | ||||||
|  |  | ||||||
| def test_register_post_empty(client): | def test_register_post_empty(client): | ||||||
|     """Test sending empty registration data |     """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 |     assert b'This field is required' in page.data | ||||||
|  |  | ||||||
| def test_register_invalid_email(client): | def test_register_invalid_email(client): | ||||||
|     """Test sending an invalid email address |     """Test sending an invalid email address | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.post('/accounts/register', data={ |     page = client.post('/register', data={ | ||||||
|         'username': 'test', |         'username': 'test', | ||||||
|         'email': 'test', |         'email': 'test', | ||||||
|         'password': 'password', |         'password': 'password', | ||||||
| @@ -53,7 +53,7 @@ def test_register_password_mismatch(client): | |||||||
|     """Test sending different password for registration |     """Test sending different password for registration | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.post('/accounts/register', data={ |     page = client.post('/register', data={ | ||||||
|         'username': 'test', |         'username': 'test', | ||||||
|         'email': 'test@example.com', |         'email': 'test@example.com', | ||||||
|         'password': 'password', |         'password': 'password', | ||||||
| @@ -65,12 +65,13 @@ def test_register(client): | |||||||
|     """Test user registration |     """Test user registration | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.post('/accounts/register', data={ |     page = client.post('/register', data={ | ||||||
|         'username': 'test', |         'username': 'test', | ||||||
|         'email': 'test@example.com', |         'email': 'test@example.com', | ||||||
|         'password': 'password', |         'password': 'password', | ||||||
|         'password_retype': 'password', |         'password_retype': 'password', | ||||||
|     }) |     }) | ||||||
|  |     print(page.data) | ||||||
|     assert page.status_code == 302 |     assert page.status_code == 302 | ||||||
|     assert page.location == 'http://localhost/' |     assert page.location == 'http://localhost/' | ||||||
|  |  | ||||||
| @@ -89,7 +90,7 @@ def test_register_existing_username(client): | |||||||
|         db.session.add(user) |         db.session.add(user) | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|  |  | ||||||
|     page = client.post('/accounts/register', data={ |     page = client.post('/register', data={ | ||||||
|         'username': 'test', |         'username': 'test', | ||||||
|         'email': 'test2@example.com', |         'email': 'test2@example.com', | ||||||
|         'password': 'password', |         'password': 'password', | ||||||
| @@ -106,16 +107,10 @@ def test_register_existing_email(client): | |||||||
|         db.session.add(user) |         db.session.add(user) | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|  |  | ||||||
|     page = client.post('/accounts/register', data={ |     page = client.post('/register', data={ | ||||||
|         'username': 'tester', |         'username': 'tester', | ||||||
|         'email': 'test@example.com', |         'email': 'test@example.com', | ||||||
|         'password': 'password', |         'password': 'password', | ||||||
|         'password_retype': 'password', |         'password_retype': 'password', | ||||||
|     }) |     }) | ||||||
|     assert b'This email address can not be used' in page.data |     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 |  | ||||||
|   | |||||||
| @@ -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 |  | ||||||
		Reference in New Issue
	
	Block a user