Compare commits
	
		
			1 Commits
		
	
	
		
			vagrant
			...
			friend-lis
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2738c5d84c | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,3 @@ __pycache__/ | |||||||
| /messages.pot | /messages.pot | ||||||
| /calsocial/translations/*/LC_MESSAGES/*.mo | /calsocial/translations/*/LC_MESSAGES/*.mo | ||||||
| /.pytest_cache/ | /.pytest_cache/ | ||||||
| /.env |  | ||||||
| /.vagrant/ |  | ||||||
| /ansible/*.retry |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -13,7 +13,6 @@ sqlalchemy-utils = "*" | |||||||
| bcrypt = "*" | bcrypt = "*" | ||||||
| flask-babelex = "*" | flask-babelex = "*" | ||||||
| python-dateutil = "*" | python-dateutil = "*" | ||||||
| flask-caching = "*" |  | ||||||
|  |  | ||||||
| [dev-packages] | [dev-packages] | ||||||
| pylint = "*" | pylint = "*" | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "e4313bc9baef5cb187176951d45094fe1de4ccba0d15ab58efbac21b6434f255" |             "sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @@ -128,14 +128,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.9.3" |             "version": "==0.9.3" | ||||||
|         }, |         }, | ||||||
|         "flask-caching": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227", |  | ||||||
|                 "sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985" |  | ||||||
|             ], |  | ||||||
|             "index": "pypi", |  | ||||||
|             "version": "==1.4.0" |  | ||||||
|         }, |  | ||||||
|         "flask-login": { |         "flask-login": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" |                 "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" | ||||||
| @@ -255,9 +247,9 @@ | |||||||
|         }, |         }, | ||||||
|         "sqlalchemy": { |         "sqlalchemy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8" |                 "sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.2.10" |             "version": "==1.2.9" | ||||||
|         }, |         }, | ||||||
|         "sqlalchemy-utils": { |         "sqlalchemy-utils": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -284,10 +276,10 @@ | |||||||
|     "develop": { |     "develop": { | ||||||
|         "astroid": { |         "astroid": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:8704779744963d56a2625ec2949eb150bd499fc099510161ddbb2b64e2d98138", |                 "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a", | ||||||
|                 "sha256:add3fd690e7c1fe92436d17be461feeaa173e6f33e0789734310334da0f30027" |                 "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a" | ||||||
|             ], |             ], | ||||||
|             "version": "==2.0" |             "version": "==1.6.5" | ||||||
|         }, |         }, | ||||||
|         "atomicwrites": { |         "atomicwrites": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -377,11 +369,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4", |                 "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c", | ||||||
|                 "sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8" |                 "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.0.0" |             "version": "==1.9.2" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -398,35 +390,6 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==1.11.0" |             "version": "==1.11.0" | ||||||
|         }, |         }, | ||||||
|         "typed-ast": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", |  | ||||||
|                 "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", |  | ||||||
|                 "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", |  | ||||||
|                 "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", |  | ||||||
|                 "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", |  | ||||||
|                 "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", |  | ||||||
|                 "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", |  | ||||||
|                 "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", |  | ||||||
|                 "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", |  | ||||||
|                 "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", |  | ||||||
|                 "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", |  | ||||||
|                 "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", |  | ||||||
|                 "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", |  | ||||||
|                 "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", |  | ||||||
|                 "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", |  | ||||||
|                 "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", |  | ||||||
|                 "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", |  | ||||||
|                 "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", |  | ||||||
|                 "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", |  | ||||||
|                 "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", |  | ||||||
|                 "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", |  | ||||||
|                 "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", |  | ||||||
|                 "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" |  | ||||||
|             ], |  | ||||||
|             "markers": "python_version < '3.7' and implementation_name == 'cpython'", |  | ||||||
|             "version": "==1.1.0" |  | ||||||
|         }, |  | ||||||
|         "wrapt": { |         "wrapt": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" |                 "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										74
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							| @@ -1,74 +0,0 @@ | |||||||
| # -*- mode: ruby -*- |  | ||||||
| # vi: set ft=ruby : |  | ||||||
|  |  | ||||||
| # All Vagrant configuration is done below. The "2" in Vagrant.configure |  | ||||||
| # configures the configuration version (we support older styles for |  | ||||||
| # backwards compatibility). Please don't change it unless you know what |  | ||||||
| # you're doing. |  | ||||||
| Vagrant.configure("2") do |config| |  | ||||||
|   # The most common configuration options are documented and commented below. |  | ||||||
|   # For a complete reference, please see the online documentation at |  | ||||||
|   # https://docs.vagrantup.com. |  | ||||||
|  |  | ||||||
|   # Every Vagrant development environment requires a box. You can search for |  | ||||||
|   # boxes at https://vagrantcloud.com/search. |  | ||||||
|   config.vm.box = 'fedora/28-cloud-base' |  | ||||||
|  |  | ||||||
|   # Disable automatic box update checking. If you disable this, then |  | ||||||
|   # boxes will only be checked for updates when the user runs |  | ||||||
|   # `vagrant box outdated`. This is not recommended. |  | ||||||
|   # config.vm.box_check_update = false |  | ||||||
|  |  | ||||||
|   # Create a forwarded port mapping which allows access to a specific port |  | ||||||
|   # within the machine from a port on the host machine. In the example below, |  | ||||||
|   # accessing "localhost:8080" will access port 80 on the guest machine. |  | ||||||
|   # NOTE: This will enable public access to the opened port |  | ||||||
|   config.vm.network "forwarded_port", guest: 80, host: 8080 |  | ||||||
|  |  | ||||||
|   # Create a forwarded port mapping which allows access to a specific port |  | ||||||
|   # within the machine from a port on the host machine and only allow access |  | ||||||
|   # via 127.0.0.1 to disable public access |  | ||||||
|   # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" |  | ||||||
|  |  | ||||||
|   # Create a private network, which allows host-only access to the machine |  | ||||||
|   # using a specific IP. |  | ||||||
|   # config.vm.network "private_network", ip: "192.168.33.10" |  | ||||||
|  |  | ||||||
|   # Create a public network, which generally matched to bridged network. |  | ||||||
|   # Bridged networks make the machine appear as another physical device on |  | ||||||
|   # your network. |  | ||||||
|   # config.vm.network "public_network" |  | ||||||
|  |  | ||||||
|   config.vm.synced_folder './', '/vagrant', type: 'sshfs' |  | ||||||
|  |  | ||||||
|   # Share an additional folder to the guest VM. The first argument is |  | ||||||
|   # the path on the host to the actual folder. The second argument is |  | ||||||
|   # the path on the guest to mount the folder. And the optional third |  | ||||||
|   # argument is a set of non-required options. |  | ||||||
|   # config.vm.synced_folder "../data", "/vagrant_data" |  | ||||||
|  |  | ||||||
|   # Provider-specific configuration so you can fine-tune various |  | ||||||
|   # backing providers for Vagrant. These expose provider-specific options. |  | ||||||
|   # Example for VirtualBox: |  | ||||||
|   # |  | ||||||
|   # config.vm.provider "virtualbox" do |vb| |  | ||||||
|   #   # Display the VirtualBox GUI when booting the machine |  | ||||||
|   #   vb.gui = true |  | ||||||
|   # |  | ||||||
|   #   # Customize the amount of memory on the VM: |  | ||||||
|   #   vb.memory = "1024" |  | ||||||
|   # end |  | ||||||
|   # |  | ||||||
|   # View the documentation for the provider you are using for more |  | ||||||
|   # information on available options. |  | ||||||
|  |  | ||||||
|   # Enable provisioning with a shell script. Additional provisioners such as |  | ||||||
|   # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the |  | ||||||
|   # documentation for more information about their specific syntax and use. |  | ||||||
|   config.vm.provision "ansible_local" do |ansible| |  | ||||||
|     ansible.compatibility_mode = '2.0' |  | ||||||
|     ansible.install = true |  | ||||||
|     ansible.provisioning_path = '/vagrant/ansible' |  | ||||||
|     ansible.playbook = 'dev.yml' |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Configuration for local development on Vagrant |  | ||||||
|   hosts: all |  | ||||||
|   become: yes |  | ||||||
|   vars: |  | ||||||
|     user_name: vagrant |  | ||||||
|     group_name: vagrant |  | ||||||
|  |  | ||||||
|   roles: |  | ||||||
|     - common |  | ||||||
|     - python |  | ||||||
|     - role: gunicorn |  | ||||||
|       autostart: false |  | ||||||
|       enabled: false |  | ||||||
|     - role: nginx |  | ||||||
|       use_ssl: false |  | ||||||
|       enabled: false |  | ||||||
|  |  | ||||||
|   tasks: |  | ||||||
|     - name: Allow virtualenv python to bind to port 80 |  | ||||||
|       command: setcap cap_net_bind_service=ep /usr/bin/python3.6 |  | ||||||
|       changed_when: false |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| #! /usr/bin/env bash |  | ||||||
|  |  | ||||||
| if [ ! -f /etc/ansible/hosts ] |  | ||||||
| then |  | ||||||
|     echo "Installing Ansible..." |  | ||||||
|     sudo dnf remove ansible |  | ||||||
|     sudo dnf install ansible-python3 |  | ||||||
|  |  | ||||||
|     printf 'localhost\n' | sudo tee /etc/ansible/hosts > /dev/null |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| echo "Ansible is installed." |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Install required packages |  | ||||||
|   dnf: |  | ||||||
|     name: "{{ item }}" |  | ||||||
|     state: present |  | ||||||
|   with_items: |  | ||||||
|     - libselinux-python |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| # Project name |  | ||||||
| project_name: calendar.social |  | ||||||
|  |  | ||||||
| # Project path |  | ||||||
| project_path: /vagrant |  | ||||||
|  |  | ||||||
| # Flask app path |  | ||||||
| application_path: /vagrant/app |  | ||||||
| @@ -1,56 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Install Supervisor |  | ||||||
|   dnf: |  | ||||||
|     name: "{{ item }}" |  | ||||||
|     state: present |  | ||||||
|   with_items: |  | ||||||
|     - supervisor |  | ||||||
|  |  | ||||||
| - name: Start supervisord |  | ||||||
|   service: |  | ||||||
|     name: supervisord |  | ||||||
|     state: restarted |  | ||||||
|  |  | ||||||
| - name: Create the Gunicorn config directory |  | ||||||
|   file: |  | ||||||
|     path: /etc/gunicorn |  | ||||||
|     state: directory |  | ||||||
|     owner: "{{ user_name }}" |  | ||||||
|     group: "{{ group_name }}" |  | ||||||
|     mode: 0700 |  | ||||||
|  |  | ||||||
| - name: Create the Gunicorn config file in /etc/gunicorn |  | ||||||
|   template: |  | ||||||
|     src: gunicorn.conf.j2 |  | ||||||
|     dest: /etc/gunicorn/gunicorn.conf |  | ||||||
|  |  | ||||||
| - name: Create the Gunicorn log directory |  | ||||||
|   file: |  | ||||||
|     path: /var/log/gunicorn |  | ||||||
|     state: directory |  | ||||||
|     owner: "{{ user_name }}" |  | ||||||
|     group: "{{ group_name }}" |  | ||||||
|     mode: 0700 |  | ||||||
|  |  | ||||||
| - name: Create the Supervisor config file for Gunicorn |  | ||||||
|   template: |  | ||||||
|     src: supervisor.conf.j2 |  | ||||||
|     dest: /etc/supervisord.d/gunicorn.ini |  | ||||||
|  |  | ||||||
| - name: Re-read the Supervisor config files |  | ||||||
|   supervisorctl: |  | ||||||
|     name: gunicorn |  | ||||||
|     state: present |  | ||||||
|  |  | ||||||
| - name: Start Gunicorn with supervisord |  | ||||||
|   supervisorctl: |  | ||||||
|     name: gunicorn |  | ||||||
|     state: restarted |  | ||||||
|   when: enabled |  | ||||||
|  |  | ||||||
| - name: Stop Gunicorn for local dev |  | ||||||
|   supervisorctl: |  | ||||||
|     name: gunicorn |  | ||||||
|     state: stopped |  | ||||||
|   when: not enabled |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| import multiprocessing |  | ||||||
|  |  | ||||||
| workers = multiprocessing.cpu_count() * 2 + 1 |  | ||||||
| proc_name = 'gunicorn' |  | ||||||
| bind = '127.0.0.1:8000' |  | ||||||
| errorlog = '/var/log/gunicorn/gunicorn-error.log' |  | ||||||
| accesslog = '/var/log/gunicorn/gunicorn-access.log' |  | ||||||
| loglevel = 'warning' |  | ||||||
| timeout = 60 |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| [program:gunicorn] |  | ||||||
| command=pipenv run gunicorn wsgi:app -c /etc/gunicorn/gunicorn.conf --pythonpath {{ application_path }} |  | ||||||
| directory={{ application_path }} |  | ||||||
| user={{ user_name }} |  | ||||||
| group={{ group_name }} |  | ||||||
| autorestart=true |  | ||||||
| autostart={{ autostart | bool | lower }} |  | ||||||
| redirect_stderr=true |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Reload Nginx |  | ||||||
|   service: |  | ||||||
|     name: nginx |  | ||||||
|     state: reloaded |  | ||||||
|  |  | ||||||
| - name: Stop Nginx |  | ||||||
|   service: |  | ||||||
|     name: nginx |  | ||||||
|     state: stopped |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Install Nginx |  | ||||||
|   dnf: |  | ||||||
|     name: "{{ item }}" |  | ||||||
|     state: present |  | ||||||
|   with_items: |  | ||||||
|     - nginx |  | ||||||
|  |  | ||||||
| - name: Create the Nginx configuration file for SSL |  | ||||||
|   template: |  | ||||||
|     src: site-ssl.conf.j2 |  | ||||||
|     dest: /etc/nginx/conf.d/{{ project_name }}-ssl.conf |  | ||||||
|   when: use_ssl |  | ||||||
|   notify: Reload Nginx |  | ||||||
|  |  | ||||||
| - name: Create the Nginx configuration file (non-SSL) |  | ||||||
|   template: |  | ||||||
|     src: site.conf.j2 |  | ||||||
|     dest: /etc/nginx/conf.d/{{ project_name }}.conf |  | ||||||
|   when: not use_ssl |  | ||||||
|   notify: Reload Nginx |  | ||||||
|  |  | ||||||
| - name: Ensure that the default site is removed |  | ||||||
|   file: |  | ||||||
|     path: /etc/nginx/conf.d/default.conf |  | ||||||
|     state: absent |  | ||||||
|  |  | ||||||
| - name: Ensure Nginx service is started, enable service on restart |  | ||||||
|   service: |  | ||||||
|     name: nginx |  | ||||||
|     state: restarted |  | ||||||
|     enabled: yes |  | ||||||
|   when: enabled |  | ||||||
|  |  | ||||||
| - name: Stop nginx for local dev, disable service |  | ||||||
|   service: |  | ||||||
|     name: nginx |  | ||||||
|     state: stopped |  | ||||||
|     enabled: no |  | ||||||
|   notify: Stop Nginx |  | ||||||
|   when: not enabled |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| upstream appserver { |  | ||||||
|   server localhost:8000 fail_timeout=0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| server { |  | ||||||
|   listen 80; |  | ||||||
|   return 301 https://$host$request_uri; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| server { |  | ||||||
|   listen 443 ssl deferred; |  | ||||||
|   server_name {{ host_name }}; |  | ||||||
|  |  | ||||||
|   ssl_certificate {{ home_path }}/{{ project_name }}.crt; |  | ||||||
|   ssl_certificate_key {{ home_path }}/{{ project_name }}.key; |  | ||||||
|   ssl_session_cache shared:SSL:32m; |  | ||||||
|   ssl_protocols TLSv1 TLSv1.1 TLSv1.2; |  | ||||||
|   ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; |  | ||||||
|   ssl_prefer_server_ciphers on; |  | ||||||
|  |  | ||||||
|   access_log /var/log/nginx/{{ project_name }}.access.log; |  | ||||||
|   error_log /var/log/nginx/{{ project_name }}.error.log info; |  | ||||||
|  |  | ||||||
|   keepalive_timeout 5; |  | ||||||
|  |  | ||||||
|   location /static { |  | ||||||
|     alias {{ project_path }}/static; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   location / { |  | ||||||
|     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |  | ||||||
|     proxy_set_header Host $http_host; |  | ||||||
|     proxy_redirect off; |  | ||||||
|     proxy_read_timeout 180s; |  | ||||||
|  |  | ||||||
|     if (!-f $request_filename) { |  | ||||||
|       proxy_pass http://appserver; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| upstream appserver { |  | ||||||
|   server localhost:8000 fail_timeout=0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| server { |  | ||||||
|   listen 80; |  | ||||||
|   server_name {{ host_name }}; |  | ||||||
|  |  | ||||||
|   access_log /var/log/nginx/{{ project_name }}.access.log; |  | ||||||
|   error_log /var/log/nginx/{{ project_name }}.error.log info; |  | ||||||
|  |  | ||||||
|   keepalive_timeout 5; |  | ||||||
|  |  | ||||||
|   location /static { |  | ||||||
|     alias {{ project_path }}/static; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   location / { |  | ||||||
|     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |  | ||||||
|     proxy_set_header Host $http_host; |  | ||||||
|     proxy_redirect off; |  | ||||||
|     proxy_read_timeout 180s; |  | ||||||
|  |  | ||||||
|     if (-f $request_filename) { |  | ||||||
|       proxy_pass http://appserver; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| host_name: calendar-social.local |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| --- |  | ||||||
|  |  | ||||||
| - name: Install common python packages |  | ||||||
|   dnf: |  | ||||||
|     name: "{{ item }}" |  | ||||||
|     state: present |  | ||||||
|   with_items: |  | ||||||
|     - pipenv |  | ||||||
|  |  | ||||||
| - name: Delete Python cache files |  | ||||||
|   command: find . -type d -name __pycache__ -exec rm -r {} + |  | ||||||
|   args: |  | ||||||
|     chdir: "{{ project_path }}" |  | ||||||
|   changed_when: false |  | ||||||
|  |  | ||||||
| - name: Install packages |  | ||||||
|   command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy |  | ||||||
|  |  | ||||||
| - name: Install development related packages |  | ||||||
|   command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy --dev |  | ||||||
|   args: |  | ||||||
|     chdir: "{{ project_path }}" |  | ||||||
| @@ -25,10 +25,6 @@ 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 | ||||||
|  |  | ||||||
| from calsocial.account import AccountBlueprint |  | ||||||
| from calsocial.cache import CachedSessionInterface, cache |  | ||||||
| from calsocial.utils import RoutedMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_locale(): | def get_locale(): | ||||||
|     """Locale selector |     """Locale selector | ||||||
| @@ -57,7 +53,22 @@ def template_vars(): | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| class CalendarSocialApp(Flask, RoutedMixin): | def route(*args, **kwargs): | ||||||
|  |     """Mark a function as a future route | ||||||
|  |  | ||||||
|  |     Such functions will be iterated over when the application is initialised.  ``*args`` and | ||||||
|  |     ``**kwargs`` will be passed verbatim to `Flask.route()`. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def decorator(func):  # pylint: disable=missing-docstring | ||||||
|  |         setattr(func, 'routing', (args, kwargs)) | ||||||
|  |  | ||||||
|  |         return func | ||||||
|  |  | ||||||
|  |     return decorator | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CalendarSocialApp(Flask): | ||||||
|     """The Calendar.social app |     """The Calendar.social app | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
| @@ -68,22 +79,13 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         Flask.__init__(self, name) |         Flask.__init__(self, name) | ||||||
|  |  | ||||||
|         self.session_interface = CachedSessionInterface() |  | ||||||
|  |  | ||||||
|         self._timezone = None |         self._timezone = None | ||||||
|  |  | ||||||
|         config_name = os.environ.get('ENV', config or 'development') |         config_name = os.environ.get('ENV', config or 'dev') | ||||||
|         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.jinja_env.policies['ext.i18n.trimmed'] = True  # pylint: disable=no-member |  | ||||||
|  |  | ||||||
|         db.init_app(self) |         db.init_app(self) | ||||||
|  |  | ||||||
|         cache.init_app(self) |  | ||||||
|  |  | ||||||
|         babel = Babel(app=self) |         babel = Babel(app=self) | ||||||
|         babel.localeselector(get_locale) |         babel.localeselector(get_locale) | ||||||
|  |  | ||||||
| @@ -94,9 +96,18 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         self.context_processor(template_vars) |         self.context_processor(template_vars) | ||||||
|  |  | ||||||
|         RoutedMixin.register_routes(self) |         for attr_name in self.__dir__(): | ||||||
|  |             attr = getattr(self, attr_name) | ||||||
|  |  | ||||||
|         AccountBlueprint().init_app(self, '/accounts/') |             if not callable(attr): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             args, kwargs = getattr(attr, 'routing', (None, None)) | ||||||
|  |  | ||||||
|  |             if args is None: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             self.route(*args, **kwargs)(attr) | ||||||
|  |  | ||||||
|         self.before_request(self.goto_first_steps) |         self.before_request(self.goto_first_steps) | ||||||
|  |  | ||||||
| @@ -108,7 +119,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 != 'first_steps': |            request.endpoint != 'first_steps': | ||||||
|             return redirect(url_for('account.first_steps')) |             return redirect(url_for('first_steps')) | ||||||
|  |  | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
| @@ -139,58 +150,60 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return self._timezone |         return self._timezone | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _current_calendar(): |     @route('/') | ||||||
|         from .calendar_system.gregorian import GregorianCalendar |     def hello(): | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             timestamp = datetime.fromtimestamp(float(request.args.get('date'))) |  | ||||||
|         except TypeError: |  | ||||||
|             timestamp = datetime.utcnow() |  | ||||||
|  |  | ||||||
|         return GregorianCalendar(timestamp.timestamp()) |  | ||||||
|  |  | ||||||
|     @RoutedMixin.route('/about') |  | ||||||
|     def about(self): |  | ||||||
|         """View for the about page |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .models import User, Event |  | ||||||
|  |  | ||||||
|         calendar = self._current_calendar() |  | ||||||
|  |  | ||||||
|         if not current_user.is_authenticated: |  | ||||||
|             login_form_class = current_app.extensions['security'].login_form |  | ||||||
|             login_form = login_form_class() |  | ||||||
|         else: |  | ||||||
|             login_form = None |  | ||||||
|  |  | ||||||
|         user_count = User.query.count() |  | ||||||
|         event_count = Event.query.count() |  | ||||||
|  |  | ||||||
|         return render_template('welcome.html', |  | ||||||
|                                calendar=calendar, |  | ||||||
|                                user_only=False, |  | ||||||
|                                login_form=login_form, |  | ||||||
|                                user_count=user_count, |  | ||||||
|                                event_count=event_count) |  | ||||||
|  |  | ||||||
|     @RoutedMixin.route('/') |  | ||||||
|     def hello(self): |  | ||||||
|         """View for the main page |         """View for the main page | ||||||
|  |  | ||||||
|         This will display a welcome message for users not logged in; for others, their main |         This will display a welcome message for users not logged in; for others, their main | ||||||
|         calendar view is displayed. |         calendar view is displayed. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         calendar = self._current_calendar() |         from .calendar_system.gregorian import GregorianCalendar | ||||||
|  |  | ||||||
|         if not current_user.is_authenticated: |         if not current_user.is_authenticated: | ||||||
|             return self.about() |             return render_template('welcome.html') | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             timestamp = datetime.fromtimestamp(float(request.args.get('date'))) | ||||||
|  |         except TypeError: | ||||||
|  |             timestamp = datetime.utcnow() | ||||||
|  |  | ||||||
|  |         calendar = GregorianCalendar(timestamp.timestamp()) | ||||||
|  |  | ||||||
|         return render_template('index.html', calendar=calendar, user_only=True) |         return render_template('index.html', calendar=calendar, user_only=True) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/new-event', methods=['GET', 'POST']) |     @route('/register', methods=['POST', 'GET']) | ||||||
|  |     def register(): | ||||||
|  |         """View for user registration | ||||||
|  |  | ||||||
|  |         If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the | ||||||
|  |         registration disabled template.  Otherwise, it performs user registration. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not current_app.config['REGISTRATION_ENABLED']: | ||||||
|  |             return render_template('registration-disabled.html') | ||||||
|  |  | ||||||
|  |         from .forms import RegistrationForm | ||||||
|  |         from .models import db, User | ||||||
|  |  | ||||||
|  |         form = RegistrationForm() | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             # TODO: This might become False later, if we want registrations to be confirmed via | ||||||
|  |             #       e-mail | ||||||
|  |             user = User(active=True) | ||||||
|  |  | ||||||
|  |             form.populate_obj(user) | ||||||
|  |             db.session.add(user) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         return render_template('registration.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/new-event', methods=['GET', 'POST']) | ||||||
|     @login_required |     @login_required | ||||||
|     def new_event(): |     def new_event(): | ||||||
|         """View for creating a new event |         """View for creating a new event | ||||||
| @@ -215,7 +228,28 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return render_template('event-edit.html', form=form) |         return render_template('event-edit.html', form=form) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/event/<string:event_uuid>', methods=['GET', 'POST']) |     @route('/settings', methods=['GET', 'POST']) | ||||||
|  |     @login_required | ||||||
|  |     def settings(): | ||||||
|  |         """View for user settings | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .forms import SettingsForm | ||||||
|  |         from .models import db | ||||||
|  |  | ||||||
|  |         form = SettingsForm(current_user) | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             form.populate_obj(current_user) | ||||||
|  |  | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         return render_template('user-settings.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/event/<string:event_uuid>', methods=['GET', 'POST']) | ||||||
|     def event_details(event_uuid): |     def event_details(event_uuid): | ||||||
|         """View to display event details |         """View to display event details | ||||||
|         """ |         """ | ||||||
| @@ -242,7 +276,7 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return render_template('event-details.html', event=event, form=form) |         return render_template('event-details.html', event=event, form=form) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/profile/@<string:username>') |     @route('/profile/@<string:username>') | ||||||
|     def display_profile(username): |     def display_profile(username): | ||||||
|         """View to display profile details |         """View to display profile details | ||||||
|         """ |         """ | ||||||
| @@ -257,7 +291,7 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return render_template('profile-details.html', profile=profile) |         return render_template('profile-details.html', profile=profile) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/profile/@<string:username>/follow') |     @route('/profile/@<string:username>/follow') | ||||||
|     @login_required |     @login_required | ||||||
|     def follow_user(username): |     def follow_user(username): | ||||||
|         """View for following a user |         """View for following a user | ||||||
| @@ -278,7 +312,22 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return redirect(url_for('display_profile', username=username)) |         return redirect(url_for('display_profile', username=username)) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/accept/<int:invite_id>') |     @route('/notifications') | ||||||
|  |     def notifications(): | ||||||
|  |         """View to list the notifications for the current user | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .models import Notification | ||||||
|  |  | ||||||
|  |         if current_user.is_authenticated: | ||||||
|  |             notifs = Notification.query.filter(Notification.profile == current_user.profile) | ||||||
|  |         else: | ||||||
|  |             notifs = [] | ||||||
|  |  | ||||||
|  |         return render_template('notifications.html', notifs=notifs) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/accept/<int:invite_id>') | ||||||
|     def accept_invite(invite_id): |     def accept_invite(invite_id): | ||||||
|         """View to accept an invitation |         """View to accept an invitation | ||||||
|         """ |         """ | ||||||
| @@ -308,7 +357,55 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|         return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) |         return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @RoutedMixin.route('/all-events') |     @route('/first-steps', methods=['GET', 'POST']) | ||||||
|  |     @login_required | ||||||
|  |     def first_steps(): | ||||||
|  |         """View to set up a new registrant’s profile | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .forms import FirstStepsForm | ||||||
|  |         from .models import db, Profile | ||||||
|  |  | ||||||
|  |         if current_user.profile: | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         form = FirstStepsForm() | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             profile = Profile(user=current_user, display_name=form.display_name.data) | ||||||
|  |             db.session.add(profile) | ||||||
|  |  | ||||||
|  |             current_user.settings['timezone'] = str(form.time_zone.data) | ||||||
|  |  | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         return render_template('first-steps.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/edit-profile', methods=['GET', 'POST']) | ||||||
|  |     @login_required | ||||||
|  |     def edit_profile(): | ||||||
|  |         """View for editing one’s profile | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .forms import ProfileForm | ||||||
|  |         from .models import db | ||||||
|  |  | ||||||
|  |         form = ProfileForm(current_user.profile) | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             form.populate_obj(current_user.profile) | ||||||
|  |             db.session.add(current_user.profile) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('edit_profile')) | ||||||
|  |  | ||||||
|  |         return render_template('profile-edit.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/all-events') | ||||||
|     def all_events(): |     def all_events(): | ||||||
|         """View for listing all available events |         """View for listing all available events | ||||||
|         """ |         """ | ||||||
| @@ -324,5 +421,45 @@ class CalendarSocialApp(Flask, RoutedMixin): | |||||||
|  |  | ||||||
|         return render_template('index.html', calendar=calendar, user_only=False) |         return render_template('index.html', calendar=calendar, user_only=False) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/follow-requests') | ||||||
|  |     @login_required | ||||||
|  |     def follow_requests(): | ||||||
|  |         """View for listing follow requests | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .models import UserFollow | ||||||
|  |  | ||||||
|  |         requests = UserFollow.query \ | ||||||
|  |                              .filter(UserFollow.followed == current_user.profile) \ | ||||||
|  |                              .filter(UserFollow.accepted_at.is_(None)) | ||||||
|  |  | ||||||
|  |         return render_template('follow-requests.html', requests=requests) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @route('/follow-request/<int:follower_id>/accept') | ||||||
|  |     @login_required | ||||||
|  |     def accept_follow(follower_id): | ||||||
|  |         """View for accepting a follow request | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .models import db, UserFollow | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             req = UserFollow.query \ | ||||||
|  |                             .filter(UserFollow.followed == current_user.profile) \ | ||||||
|  |                             .filter(UserFollow.follower_id == follower_id) \ | ||||||
|  |                             .one() | ||||||
|  |         except NoResultFound: | ||||||
|  |             abort(404) | ||||||
|  |  | ||||||
|  |         if req.accepted_at is None: | ||||||
|  |             req.accept() | ||||||
|  |  | ||||||
|  |             db.session.add(req) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |         return redirect(url_for('follow_requests')) | ||||||
|  |  | ||||||
|  |  | ||||||
| app = CalendarSocialApp(__name__) | app = CalendarSocialApp(__name__) | ||||||
|   | |||||||
| @@ -6,4 +6,4 @@ from calsocial import CalendarSocialApp | |||||||
|  |  | ||||||
| app = CalendarSocialApp('calsocial') | app = CalendarSocialApp('calsocial') | ||||||
|  |  | ||||||
| app.run(host='0.0.0.0', port=80) | app.run() | ||||||
|   | |||||||
| @@ -1,234 +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/>. |  | ||||||
|  |  | ||||||
| """Main module for the Calendar.social app |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| from flask import Blueprint, abort, current_app, flash, redirect, render_template, session, url_for |  | ||||||
| from flask_babelex import gettext as _ |  | ||||||
| from flask_security import current_user, login_required |  | ||||||
|  |  | ||||||
| from sqlalchemy.orm.exc import NoResultFound |  | ||||||
|  |  | ||||||
| from calsocial.utils import RoutedMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AccountBlueprint(Blueprint, RoutedMixin): |  | ||||||
|     """Blueprint for account management |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__(self): |  | ||||||
|         Blueprint.__init__(self, 'account', __name__) |  | ||||||
|  |  | ||||||
|         self.app = None |  | ||||||
|  |  | ||||||
|         RoutedMixin.register_routes(self) |  | ||||||
|  |  | ||||||
|     def init_app(self, app, url_prefix=None): |  | ||||||
|         """Initialise the blueprint, registering it with ``app``. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self.app = app |  | ||||||
|  |  | ||||||
|         app.register_blueprint(self, url_prefix=url_prefix) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/register') |  | ||||||
|     def register_account(): |  | ||||||
|         """View for user registration |  | ||||||
|  |  | ||||||
|         If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the |  | ||||||
|         registration disabled template.  Otherwise, it performs user registration. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if not current_app.config['REGISTRATION_ENABLED']: |  | ||||||
|             return render_template('registration-disabled.html') |  | ||||||
|  |  | ||||||
|         from .forms import RegistrationForm |  | ||||||
|         from .models import db, User |  | ||||||
|  |  | ||||||
|         form = RegistrationForm() |  | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |  | ||||||
|             # TODO: This might become False later, if we want registrations to be confirmed via |  | ||||||
|             #       e-mail |  | ||||||
|             user = User(active=True) |  | ||||||
|  |  | ||||||
|             form.populate_obj(user) |  | ||||||
|             db.session.add(user) |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         return render_template('account/registration.html', form=form) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/settings', methods=['GET', 'POST']) |  | ||||||
|     @login_required |  | ||||||
|     def settings(): |  | ||||||
|         """View for user settings |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .forms import SettingsForm |  | ||||||
|         from .models import db |  | ||||||
|  |  | ||||||
|         form = SettingsForm(current_user) |  | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |  | ||||||
|             form.populate_obj(current_user) |  | ||||||
|  |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         return render_template('account/user-settings.html', form=form) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/notifications') |  | ||||||
|     def notifications(): |  | ||||||
|         """View to list the notifications for the current user |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .models import Notification |  | ||||||
|  |  | ||||||
|         if current_user.is_authenticated: |  | ||||||
|             notifs = Notification.query.filter(Notification.profile == current_user.profile) |  | ||||||
|         else: |  | ||||||
|             notifs = [] |  | ||||||
|  |  | ||||||
|         return render_template('account/notifications.html', notifs=notifs) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/first-steps', methods=['GET', 'POST']) |  | ||||||
|     @login_required |  | ||||||
|     def first_steps(): |  | ||||||
|         """View to set up a new registrant’s profile |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .forms import FirstStepsForm |  | ||||||
|         from .models import db, Profile |  | ||||||
|  |  | ||||||
|         if current_user.profile: |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         form = FirstStepsForm() |  | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |  | ||||||
|             profile = Profile(user=current_user, display_name=form.display_name.data) |  | ||||||
|             db.session.add(profile) |  | ||||||
|  |  | ||||||
|             current_user.settings['timezone'] = str(form.time_zone.data) |  | ||||||
|  |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         return render_template('account/first-steps.html', form=form) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/edit-profile', methods=['GET', 'POST']) |  | ||||||
|     @login_required |  | ||||||
|     def edit_profile(): |  | ||||||
|         """View for editing one’s profile |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .forms import ProfileForm |  | ||||||
|         from .models import db |  | ||||||
|  |  | ||||||
|         form = ProfileForm(current_user.profile) |  | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |  | ||||||
|             form.populate_obj(current_user.profile) |  | ||||||
|             db.session.add(current_user.profile) |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('account.edit_profile')) |  | ||||||
|  |  | ||||||
|         return render_template('account/profile-edit.html', form=form) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/follow-requests') |  | ||||||
|     @login_required |  | ||||||
|     def follow_requests(): |  | ||||||
|         """View for listing follow requests |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .models import UserFollow |  | ||||||
|  |  | ||||||
|         requests = UserFollow.query \ |  | ||||||
|                              .filter(UserFollow.followed == current_user.profile) \ |  | ||||||
|                              .filter(UserFollow.accepted_at.is_(None)) |  | ||||||
|  |  | ||||||
|         return render_template('account/follow-requests.html', requests=requests) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/follow-request/<int:follower_id>/accept') |  | ||||||
|     @login_required |  | ||||||
|     def accept_follow(follower_id): |  | ||||||
|         """View for accepting a follow request |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .models import db, UserFollow |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             req = UserFollow.query \ |  | ||||||
|                             .filter(UserFollow.followed == current_user.profile) \ |  | ||||||
|                             .filter(UserFollow.follower_id == follower_id) \ |  | ||||||
|                             .one() |  | ||||||
|         except NoResultFound: |  | ||||||
|             abort(404) |  | ||||||
|  |  | ||||||
|         if req.accepted_at is None: |  | ||||||
|             req.accept() |  | ||||||
|  |  | ||||||
|             db.session.add(req) |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|         return redirect(url_for('account.follow_requests')) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/sessions') |  | ||||||
|     @login_required |  | ||||||
|     def active_sessions(): |  | ||||||
|         """View the list of active sessions |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         sessions = [current_app.session_interface.load_session(sid) |  | ||||||
|                     for sid in current_user.active_sessions] |  | ||||||
|  |  | ||||||
|         return render_template('account/active-sessions.html', sessions=sessions) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @RoutedMixin.route('/sessions/invalidate/<string:sid>') |  | ||||||
|     @login_required |  | ||||||
|     def invalidate_session(sid): |  | ||||||
|         """View to invalidate a session |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         sess = current_app.session_interface.load_session(sid) |  | ||||||
|  |  | ||||||
|         if not sess or sess.user != current_user: |  | ||||||
|             abort(404) |  | ||||||
|  |  | ||||||
|         if sess.sid == session.sid: |  | ||||||
|             flash(_('Can’t invalidate your current session')) |  | ||||||
|         else: |  | ||||||
|             current_app.session_interface.delete_session(sid) |  | ||||||
|             current_user.active_sessions = [sess_id |  | ||||||
|                                             for sess_id in current_user.active_sessions |  | ||||||
|                                             if sess_id != sid] |  | ||||||
|  |  | ||||||
|         return redirect(url_for('account.active_sessions')) |  | ||||||
| @@ -1,153 +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/>. |  | ||||||
|  |  | ||||||
| """Caching functionality for Calendar.social |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| from datetime import timedelta |  | ||||||
| import pickle |  | ||||||
| from uuid import uuid4 |  | ||||||
|  |  | ||||||
| from flask import current_app, has_request_context, request, session |  | ||||||
| from flask.sessions import SessionInterface, SessionMixin |  | ||||||
| from flask_caching import Cache |  | ||||||
| from werkzeug.datastructures import CallbackDict |  | ||||||
|  |  | ||||||
| cache = Cache()  # pylint: disable=invalid-name |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CachedSession(CallbackDict, SessionMixin):  # pylint: disable=too-many-ancestors |  | ||||||
|     """Object for session data saved in the cache |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__(self, initial=None, sid=None, new=False): |  | ||||||
|         self.__modifying = False |  | ||||||
|  |  | ||||||
|         def on_update(self): |  | ||||||
|             """Function to call when session data is updated |  | ||||||
|             """ |  | ||||||
|  |  | ||||||
|             if self.__modifying: |  | ||||||
|                 return |  | ||||||
|  |  | ||||||
|             self.__modifying = True |  | ||||||
|  |  | ||||||
|             if has_request_context(): |  | ||||||
|                 self['ip'] = request.remote_addr |  | ||||||
|  |  | ||||||
|             self.modified = True |  | ||||||
|  |  | ||||||
|             self.__modifying = False |  | ||||||
|  |  | ||||||
|         CallbackDict.__init__(self, initial, on_update) |  | ||||||
|         self.sid = sid |  | ||||||
|         self.new = new |  | ||||||
|         self.modified = False |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def user(self): |  | ||||||
|         from calsocial.models import User |  | ||||||
|  |  | ||||||
|         if 'user_id' not in self: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         return User.query.get(self['user_id']) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CachedSessionInterface(SessionInterface): |  | ||||||
|     """A session interface that loads/saves session data from the cache |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     serializer = pickle |  | ||||||
|     session_class = CachedSession |  | ||||||
|     global_cache = cache |  | ||||||
|  |  | ||||||
|     def __init__(self, prefix='session:'): |  | ||||||
|         self.cache = cache |  | ||||||
|         self.prefix = prefix |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def generate_sid(): |  | ||||||
|         """Generade a new session ID |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         return str(uuid4()) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def get_cache_expiration_time(app, session): |  | ||||||
|         """Get the expiration time of the cache entry |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if session.permanent: |  | ||||||
|             return app.permanent_session_lifetime |  | ||||||
|  |  | ||||||
|         return timedelta(days=1) |  | ||||||
|  |  | ||||||
|     def open_session(self, app, request): |  | ||||||
|         sid = request.cookies.get(app.session_cookie_name) |  | ||||||
|  |  | ||||||
|         if not sid: |  | ||||||
|             sid = self.generate_sid() |  | ||||||
|  |  | ||||||
|             return self.session_class(sid=sid, new=True) |  | ||||||
|  |  | ||||||
|         session = self.load_session(sid) |  | ||||||
|  |  | ||||||
|         if session is None: |  | ||||||
|             return self.session_class(sid=sid, new=True) |  | ||||||
|  |  | ||||||
|         return session |  | ||||||
|  |  | ||||||
|     def load_session(self, sid): |  | ||||||
|         """Load a specific session from the cache |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         val = self.cache.get(self.prefix + sid) |  | ||||||
|  |  | ||||||
|         if val is None: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         data = self.serializer.loads(val) |  | ||||||
|  |  | ||||||
|         return self.session_class(data, sid=sid) |  | ||||||
|  |  | ||||||
|     def save_session(self, app, session, response): |  | ||||||
|         domain = self.get_cookie_domain(app) |  | ||||||
|  |  | ||||||
|         if not session: |  | ||||||
|             self.cache.delete(self.prefix + session.sid) |  | ||||||
|  |  | ||||||
|             if session.modified: |  | ||||||
|                 response.delete_cookie(app.session_cookie_name, domain=domain) |  | ||||||
|  |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         cache_exp = self.get_cache_expiration_time(app, session) |  | ||||||
|         cookie_exp = self.get_expiration_time(app, session) |  | ||||||
|         val = self.serializer.dumps(dict(session)) |  | ||||||
|         self.cache.set(self.prefix + session.sid, val, int(cache_exp.total_seconds())) |  | ||||||
|  |  | ||||||
|         response.set_cookie(app.session_cookie_name, |  | ||||||
|                             session.sid, |  | ||||||
|                             expires=cookie_exp, |  | ||||||
|                             httponly=True, |  | ||||||
|                             domain=domain) |  | ||||||
|  |  | ||||||
|     def delete_session(self, sid): |  | ||||||
|         if has_request_context() and session.sid == sid: |  | ||||||
|             raise ValueError('Will not delete the current session') |  | ||||||
|  |  | ||||||
|         cache.delete(self.prefix + sid) |  | ||||||
| @@ -83,23 +83,17 @@ class GregorianCalendar(CalendarSystem): | |||||||
|     def days(self): |     def days(self): | ||||||
|         day_list = [] |         day_list = [] | ||||||
|  |  | ||||||
|         month_first = self.timestamp.replace(day=1) |         start_day = self.timestamp.replace(day=1) | ||||||
|  |  | ||||||
|         if self.timestamp.month == 12: |         while start_day.weekday() > self.START_DAY: | ||||||
|             month_last = month_first.replace(day=31) |             start_day -= timedelta(days=1) | ||||||
|         else: |  | ||||||
|             month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1) |  | ||||||
|  |  | ||||||
|         pad_before = (7 - self.START_DAY + month_first.weekday()) % 7 |         day_list.append(start_day) | ||||||
|         pad_after = (6 - month_last.weekday() + self.START_DAY) % 7 |         current_day = start_day | ||||||
|  |  | ||||||
|         first_display = month_first - timedelta(days=pad_before) |         while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month: | ||||||
|         last_display = month_last + timedelta(days=pad_after) |             current_day += timedelta(days=1) | ||||||
|         current = first_display |             day_list.append(current_day) | ||||||
|  |  | ||||||
|         while current <= last_display: |  | ||||||
|             day_list.append(current) |  | ||||||
|             current += timedelta(days=1) |  | ||||||
|  |  | ||||||
|         return day_list |         return day_list | ||||||
|  |  | ||||||
| @@ -205,29 +199,21 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         """Returns all events for a given day |         """Returns all events for a given day | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from ..models import Event, EventVisibility, Invitation, Profile, Response |         from ..models import Event, Profile | ||||||
|  |  | ||||||
|         events = Event.query |         events = Event.query | ||||||
|  |  | ||||||
|         if user: |         if user: | ||||||
|             events = events.outerjoin(Invitation) \ |             events = events.join(Profile) \ | ||||||
|                            .outerjoin(Response) \ |  | ||||||
|                            .join(Profile, Event.profile) \ |  | ||||||
|                            .filter(Profile.user == user) |                            .filter(Profile.user == user) | ||||||
|  |  | ||||||
|         start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) |         start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) | ||||||
|         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 >= start_timestamp) & | ||||||
|                                 (Event.end_time >= start_timestamp)) \ |                                 (Event.start_time < end_timestamp)) | | ||||||
|  |                                ((Event.end_time >= start_timestamp) & | ||||||
|  |                                 (Event.end_time < end_timestamp))) \ | ||||||
|                        .order_by('start_time', 'end_time') |                        .order_by('start_time', 'end_time') | ||||||
|  |  | ||||||
|         if user is None: |  | ||||||
|             events = events.filter(Event.visibility == EventVisibility.public) |  | ||||||
|         else: |  | ||||||
|             events = events.filter((Event.visibility == EventVisibility.public) | |  | ||||||
|                                    (Event.profile == user.profile) | |  | ||||||
|                                    (Invitation.invitee == user.profile) | |  | ||||||
|                                    (Response.profile == user.profile)) |  | ||||||
|  |  | ||||||
|         return events |         return events | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Configuration file for the development environment | """Configuration file for the development environment | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| ENV = 'development' | ENV = 'dev' | ||||||
| #: If ``True``, registration on the site is enabled. | #: If ``True``, registration on the site is enabled. | ||||||
| REGISTRATION_ENABLED = True | REGISTRATION_ENABLED = True | ||||||
| #: The default time zone | #: The default time zone | ||||||
| @@ -14,4 +14,3 @@ SECRET_KEY = 'ThisIsNotSoSecret' | |||||||
| SECURITY_PASSWORD_HASH = 'bcrypt' | SECURITY_PASSWORD_HASH = 'bcrypt' | ||||||
| SECURITY_PASSWORD_SALT = SECRET_KEY | SECURITY_PASSWORD_SALT = SECRET_KEY | ||||||
| SECURITY_REGISTERABLE = False | SECURITY_REGISTERABLE = False | ||||||
| CACHE_TYPE = 'simple' |  | ||||||
| @@ -17,8 +17,6 @@ | |||||||
| """Forms for Calendar.social | """Forms for Calendar.social | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from enum import Enum |  | ||||||
|  |  | ||||||
| from flask_babelex import lazy_gettext as _ | 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 | ||||||
| @@ -28,8 +26,6 @@ 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 | ||||||
|  |  | ||||||
| from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UsernameAvailable(object):  # pylint: disable=too-few-public-methods | class UsernameAvailable(object):  # pylint: disable=too-few-public-methods | ||||||
|     """Checks if a username is available |     """Checks if a username is available | ||||||
| @@ -173,45 +169,6 @@ class TimezoneField(SelectField): | |||||||
|             yield (value, label, value == self.data) |             yield (value, label, value == self.data) | ||||||
|  |  | ||||||
|  |  | ||||||
| class EnumField(SelectField): |  | ||||||
|     """Field that allows selecting one value from an ``Enum`` class |  | ||||||
|  |  | ||||||
|     :param enum_type: an ``Enum`` type |  | ||||||
|     :type enum_type: type(Enum) |  | ||||||
|     :param translations: translatable labels for enum values |  | ||||||
|     :type translations: dict |  | ||||||
|     :param args: passed verbatim to the constructor of `SelectField` |  | ||||||
|     :param kwargs: passed verbatim to the constructor of `SelectField` |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__(self, enum_type, translations, *args, **kwargs): |  | ||||||
|         if not issubclass(enum_type, Enum): |  | ||||||
|             raise TypeError('enum_type must be a subclass of Enum') |  | ||||||
|  |  | ||||||
|         kwargs.update({'choices': [(value, None) for value in enum_type]}) |  | ||||||
|         self.data = None |  | ||||||
|         self.enum_type = enum_type |  | ||||||
|         self.translations = translations |  | ||||||
|         SelectField.__init__(self, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def process_formdata(self, valuelist): |  | ||||||
|         if not valuelist: |  | ||||||
|             self.data = None |  | ||||||
|  |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             self.data = self.enum_type[valuelist[0]] |  | ||||||
|         except KeyError: |  | ||||||
|             raise ValueError('Unknown value') |  | ||||||
|  |  | ||||||
|     def iter_choices(self): |  | ||||||
|         for value in self.enum_type: |  | ||||||
|             label = self.gettext(self.translations[value]) if self.translations else value.name |  | ||||||
|  |  | ||||||
|             yield (value.name, label, value == self.data) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventForm(FlaskForm): | class EventForm(FlaskForm): | ||||||
|     """Form for event creation/editing |     """Form for event creation/editing | ||||||
|     """ |     """ | ||||||
| @@ -222,14 +179,6 @@ class EventForm(FlaskForm): | |||||||
|     end_time = DateTimeField(_('End time'), validators=[DataRequired()]) |     end_time = DateTimeField(_('End time'), validators=[DataRequired()]) | ||||||
|     all_day = BooleanField(_('All day')) |     all_day = BooleanField(_('All day')) | ||||||
|     description = StringField(_('Description'), widget=TextArea()) |     description = StringField(_('Description'), widget=TextArea()) | ||||||
|     visibility = EnumField(EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, label=_('Visibility')) |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         from flask_security import current_user |  | ||||||
|  |  | ||||||
|         self.time_zone.kwargs['default'] = current_user.timezone  # pylint: disable=no-member |  | ||||||
|  |  | ||||||
|         FlaskForm.__init__(self, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def populate_obj(self, obj): |     def populate_obj(self, obj): | ||||||
|         """Populate ``obj`` with event data |         """Populate ``obj`` with event data | ||||||
|   | |||||||
| @@ -27,7 +27,6 @@ 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 .cache import cache |  | ||||||
| from .utils import force_locale | from .utils import force_locale | ||||||
|  |  | ||||||
| db = SQLAlchemy() | db = SQLAlchemy() | ||||||
| @@ -104,23 +103,6 @@ class ResponseType(Enum): | |||||||
|         return Enum.__eq__(self, other) |         return Enum.__eq__(self, other) | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventVisibility(Enum): |  | ||||||
|     """Enumeration for event visibility |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     #: The event is private, only attendees and people invited can see the details |  | ||||||
|     private = 0 |  | ||||||
|  |  | ||||||
|     #: The event is public, anyone can see the details |  | ||||||
|     public = 5 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| EVENT_VISIBILITY_TRANSLATIONS = { |  | ||||||
|     EventVisibility.private: _('Visible only to attendees'), |  | ||||||
|     EventVisibility.public: _('Visible to everyone'), |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SettingsProxy: | class SettingsProxy: | ||||||
|     """Proxy object to get settings for a user |     """Proxy object to get settings for a user | ||||||
|     """ |     """ | ||||||
| @@ -220,24 +202,6 @@ class User(db.Model, UserMixin): | |||||||
|  |  | ||||||
|         return current_app.timezone |         return current_app.timezone | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def session_list_key(self): |  | ||||||
|         """The cache key of this user’s session list |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         return f'open_sessions:{self.id}' |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def active_sessions(self): |  | ||||||
|         """The list of active sessions of this user |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         return cache.get(self.session_list_key) or [] |  | ||||||
|  |  | ||||||
|     @active_sessions.setter |  | ||||||
|     def active_sessions(self, value): |  | ||||||
|         cache.set(self.session_list_key, list(value)) |  | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return f'<User {self.id}({self.username})>' |         return f'<User {self.id}({self.username})>' | ||||||
|  |  | ||||||
| @@ -346,6 +310,21 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|             .filter(UserFollow.followed == self) \ |             .filter(UserFollow.followed == self) \ | ||||||
|             .filter(UserFollow.accepted_at.isnot(None)) |             .filter(UserFollow.accepted_at.isnot(None)) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def friend_list(self): | ||||||
|  |         """List of friends (ie. where both profiles follow each other) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # This will always be empty for remote profiles | ||||||
|  |         if not self.user: | ||||||
|  |             return [] | ||||||
|  |  | ||||||
|  |         reverse = db.aliased(UserFollow) | ||||||
|  |         return UserFollow.query \ | ||||||
|  |                          .filter(UserFollow.follower == self) \ | ||||||
|  |                          .join(reverse, UserFollow.followed == reverse.follower) \ | ||||||
|  |                          .filter(UserFollow.follower == reverse.followed) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def url(self): |     def url(self): | ||||||
|         """Get the URL for this profile |         """Get the URL for this profile | ||||||
| @@ -435,9 +414,6 @@ class Event(db.Model): | |||||||
|     #: The description of the event |     #: The description of the event | ||||||
|     description = db.Column(db.UnicodeText()) |     description = db.Column(db.UnicodeText()) | ||||||
|  |  | ||||||
|     #: The visibility of the event |  | ||||||
|     visibility = db.Column(db.Enum(EventVisibility), nullable=False) |  | ||||||
|  |  | ||||||
|     def __as_tz(self, timestamp, as_timezone=None): |     def __as_tz(self, timestamp, as_timezone=None): | ||||||
|         from pytz import timezone, utc |         from pytz import timezone, utc | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
| """Security related things for Calendar.social | """Security related things for Calendar.social | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from flask import current_app, session | from flask import current_app | ||||||
| from flask_login.signals import user_logged_in, user_logged_out | from flask_login.signals import user_logged_in, user_logged_out | ||||||
| from flask_security import Security, AnonymousUser as BaseAnonymousUser | from flask_security import Security, AnonymousUser as BaseAnonymousUser | ||||||
|  |  | ||||||
| @@ -45,8 +45,6 @@ def login_handler(app, user):  # pylint: disable=unused-argument | |||||||
|  |  | ||||||
|     AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) |     AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) | ||||||
|  |  | ||||||
|     user.active_sessions += [session.sid] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @user_logged_out.connect | @user_logged_out.connect | ||||||
| def logout_handler(app, user):  # pylint: disable=unused-argument | def logout_handler(app, user):  # pylint: disable=unused-argument | ||||||
|   | |||||||
| @@ -1,121 +0,0 @@ | |||||||
| header > h1 > img { |  | ||||||
|     height: 1em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #content { |  | ||||||
|     margin-top: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ui.profile { |  | ||||||
|     display: block; |  | ||||||
|     position: relative; |  | ||||||
|     height: 50px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ui.profile > .avatar { |  | ||||||
|     width: 48px; |  | ||||||
|     height: 48px; |  | ||||||
|     border: 1px solid black; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     position: absolute; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ui.profile > .display.name { |  | ||||||
|     position: absolute; |  | ||||||
|     left: 58px; |  | ||||||
|     font-weight: bold; |  | ||||||
|     color: #000000; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ui.profile > .handle { |  | ||||||
|     position: absolute; |  | ||||||
|     top: 1.5em; |  | ||||||
|     left: 58px; |  | ||||||
|     color: #666666; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ui.centered.statistics { |  | ||||||
|     justify-content: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .timezone-warning { |  | ||||||
|     color: #e94a4a; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| footer { |  | ||||||
|     margin-top: 3em; |  | ||||||
|     font-weight: bold; |  | ||||||
|     border-top: 1px dotted black; |  | ||||||
|     padding-top: 1em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media not speech { |  | ||||||
|     .sr-only { |  | ||||||
|         display: none; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.calendar > * { |  | ||||||
|     font-family: sans; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.calendar { |  | ||||||
|     width: 100%; |  | ||||||
|     border-collapse: collapse; |  | ||||||
|     table-layout: fixed; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.month > td { |  | ||||||
|     text-align: center; |  | ||||||
|     font-weight: bold; |  | ||||||
|     border-bottom: 2px solid black; |  | ||||||
|     padding-bottom: .5em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.month > td > a { |  | ||||||
|     font-weight: normal; |  | ||||||
|     color: black; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.month > td.month-name > a { |  | ||||||
|     font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.days > td { |  | ||||||
|     text-align: center; |  | ||||||
|     border-bottom: 2px solid black; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.week > td { |  | ||||||
|     height: 3em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.week > td > span.day-num { |  | ||||||
|     font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.week > td.other-month > span.day-num { |  | ||||||
|     font-weight: normal; |  | ||||||
|     color: #909090; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.week > td.today { |  | ||||||
|     background-color: #d8d8d8; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.week > td > a.event { |  | ||||||
|     display: block; |  | ||||||
|     color: black; |  | ||||||
|     text-decoration: none; |  | ||||||
|     border: 1px solid green; |  | ||||||
|     background-color: white; |  | ||||||
|     border-radius: 2px; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     white-space: nowrap; |  | ||||||
|     overflow: hidden; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tr.sizer > td { |  | ||||||
|     width: 14.2857%; |  | ||||||
|     height: 0; |  | ||||||
| } |  | ||||||
							
								
								
									
										364
									
								
								calsocial/static/semantic/semantic.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										364
									
								
								calsocial/static/semantic/semantic.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										11
									
								
								calsocial/static/semantic/semantic.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								calsocial/static/semantic/semantic.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,20 +0,0 @@ | |||||||
| {% macro field(field, inline=false) %} |  | ||||||
| <div class="{% if inline %}inline {% endif %}field"> |  | ||||||
|     {% if field.errors %} |  | ||||||
|         {% for error in field.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|         {% endfor %} |  | ||||||
|     <br> |  | ||||||
|     {% endif %} |  | ||||||
|     {% if field.widget.input_type != 'checkbox' %} |  | ||||||
|     {{ field.label }} |  | ||||||
|     {% endif %} |  | ||||||
|     {{ field }} |  | ||||||
|     {% if field.widget.input_type == 'checkbox' %} |  | ||||||
|     {{ field.label }}<br> |  | ||||||
|     {% endif %} |  | ||||||
|     {% if field.description %} |  | ||||||
|     {{ field.description }} |  | ||||||
|     {% endif %} |  | ||||||
| </div> |  | ||||||
| {% endmacro %} |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| {% extends 'account/settings-base.html' %} |  | ||||||
|  |  | ||||||
| {% block settings_content %} |  | ||||||
| <h2>{% trans %}Active sessions{% endtrans %}</h2> |  | ||||||
| <ul> |  | ||||||
|     {% for sess in sessions %} |  | ||||||
|     <li> |  | ||||||
|         {{ sess['ip'] }} |  | ||||||
|         {% if sess.sid != session.sid %} |  | ||||||
|         <a href="{{ url_for('account.invalidate_session', sid=sess.sid) }}">{% trans %}Invalidate{% endtrans %}</a> |  | ||||||
|         {% endif %} |  | ||||||
|     </li> |  | ||||||
|     {% endfor %} |  | ||||||
| </ul> |  | ||||||
| {% endblock settings_content %} |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <h1>{% trans %}First steps{% endtrans %}</h1> |  | ||||||
| <p> |  | ||||||
|     {% trans %}Welcome to Calendar.social!{% endtrans %} |  | ||||||
| </p> |  | ||||||
| <p> |  | ||||||
|     {% trans %}These are the first steps you should make before you can start using the site.{% endtrans %} |  | ||||||
| </p> |  | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {% if form.errors %} |  | ||||||
|     {% for error in form.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|     {% endfor %} |  | ||||||
|     <br> |  | ||||||
|     {% endif %} |  | ||||||
|  |  | ||||||
|     {{ field(form.display_name) }} |  | ||||||
|     {{ field(form.time_zone) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock content %} |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <h2>{% trans %}Follow requests{% endtrans %}</h2> |  | ||||||
|     {% if requests.count() %} |  | ||||||
| <ul> |  | ||||||
|         {% for req in requests %} |  | ||||||
|     <li> |  | ||||||
|         {{ req.follower }} |  | ||||||
|         <a href="{{ url_for('account.accept_follow', follower_id=req.follower_id) }}" class="ui button">{% trans %}Accept{% endtrans %}</a> |  | ||||||
|     </li> |  | ||||||
|         {% endfor %} |  | ||||||
| </ul> |  | ||||||
|     {% else %} |  | ||||||
| {% trans %}No requests to display.{% endtrans %} |  | ||||||
|     {% endif %} |  | ||||||
|     {% if not current_user.profile.locked %} |  | ||||||
| <p> |  | ||||||
|     {% trans %}Your profile is not locked.{% endtrans %} |  | ||||||
|     {% trans %}Anyone can follow you without your consent.{% endtrans %} |  | ||||||
| </p> |  | ||||||
|     {% endif %} |  | ||||||
| {% endblock content %} |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| {% extends 'account/settings-base.html' %} |  | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block settings_content %} |  | ||||||
| <h2>{% trans %}Edit profile{% endtrans %}</h2> |  | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {% if form.errors %} |  | ||||||
|     {% for error in form.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|     {% endfor %} |  | ||||||
|     <br> |  | ||||||
|     {% endif %} |  | ||||||
|  |  | ||||||
|     {{ field(form.display_name) }} |  | ||||||
|     {{ field(form.locked, inline=true) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock settings_content %} |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {% for error in form.errors %} |  | ||||||
|     {{ form.errors }} |  | ||||||
|     {% endfor %} |  | ||||||
|  |  | ||||||
|     {{ field(form.username) }} |  | ||||||
|     {{ field(form.email) }} |  | ||||||
|     {{ field(form.password) }} |  | ||||||
|     {{ field(form.password_retype) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Register{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock %} |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <div class="ui grid"> |  | ||||||
|     <div class="four wide column"> |  | ||||||
|         <div class="ui secondary pointing vertical menu"> |  | ||||||
|             <a class="item{% if request.endpoint == 'account.edit_profile' %} active{% endif %}" href="{{ url_for('account.edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a> |  | ||||||
|             <a class="item{% if request.endpoint == 'account.settings' %} active{% endif %}" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a> |  | ||||||
|             <a class="item{% if request.endpoint == 'account.active_sessions' %} active{% endif %}" href="{{ url_for('account.active_sessions') }}">{% trans %}Active sessions{% endtrans %}</a> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="twelve wide stretched column"> |  | ||||||
|     {% block settings_content %}{% endblock %} |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| {% endblock %} |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| {% extends 'account/settings-base.html' %} |  | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block settings_content %} |  | ||||||
| <h2>{% trans %}Settings{% endtrans %}</h2> |  | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {% if form.errors %} |  | ||||||
|     {% for error in form.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|     {% endfor %} |  | ||||||
|     <br> |  | ||||||
|     {% endif %} |  | ||||||
|  |  | ||||||
|     {{ field(form.timezone) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock settings_content %} |  | ||||||
| @@ -7,49 +7,47 @@ | |||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}"> | ||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}"> | ||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}"> | ||||||
|  | {% block head %} | ||||||
|  |         <style> | ||||||
|  |          header > h1 > img { | ||||||
|  |              height: 1em; | ||||||
|  |          } | ||||||
|  |  | ||||||
|         <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous"> |          footer { | ||||||
|         <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}"> |              margin-top: 3em; | ||||||
|  |              font-weight: bold; | ||||||
|         <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css"> |              border-top: 1px dotted black; | ||||||
|  |              padding-top: 1em; | ||||||
|         <meta charset="utf-8"> |          } | ||||||
|         <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |         </style> | ||||||
| {% block head %}{% endblock %} | {% endblock %} | ||||||
|     </head> |     </head> | ||||||
|     <body> |     <body> | ||||||
|         <header> |         <header> | ||||||
|             <div class="ui top attached menu"> |             <h1> | ||||||
|                 <div class="header item"> |                 <img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}"> | ||||||
|                     <img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}"> |                 Calendar.social | ||||||
|                     Calendar.social |             </h1> | ||||||
|                 </div> |             <nav class="menu"> | ||||||
|                 <div class="right menu"> | {% if current_user.is_authenticated %} | ||||||
| {% if not current_user.is_authenticated %} |                 {{ _('Logged in as %(username)s', username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe) }} | ||||||
|                     <a class="item" href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a> |  | ||||||
| {% else %} |  | ||||||
|                     <div class="item"> |  | ||||||
|     {% trans username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe -%} |  | ||||||
|                         Logged in as {{username}} |  | ||||||
|     {%- endtrans %} |  | ||||||
|                     </div> |  | ||||||
|                     <a class="item" href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a> |  | ||||||
|                     <a class="item" href="{{ url_for('account.notifications') }}">{% trans %}Notifications{% endtrans %}</a> |  | ||||||
|                     <a class="item" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a> |  | ||||||
|                     <a class="item" href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a> |  | ||||||
| {% endif %} | {% endif %} | ||||||
|                 </div> |                 <ul> | ||||||
|             </div> | {% if not current_user.is_authenticated %} | ||||||
|  |                     <li><a href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a></li> | ||||||
|  | {% else %} | ||||||
|  |                     <li><a href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a></li> | ||||||
|  |                     <li><a href="{{ url_for('notifications') }}">{% trans %}Notifications{% endtrans %}</a></li> | ||||||
|  |                     <li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li> | ||||||
|  |                     <li><a href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a></li> | ||||||
|  | {% endif %} | ||||||
|  |                 </ul> | ||||||
|  |             </nav> | ||||||
|         </header> |         </header> | ||||||
|         <div class="ui container" id="content"> |  | ||||||
| {% block content %}{% endblock %} | {% block content %}{% endblock %} | ||||||
|         </div> |         <footer> | ||||||
|         <footer class="ui segment"> |  | ||||||
|             <a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br> |  | ||||||
|             Soon…™ |             Soon…™ | ||||||
|         </footer> |         </footer> | ||||||
| {% block scripts %}{% endblock %} | {% block scripts %}{% endblock %} | ||||||
|         <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script> |  | ||||||
|         <script src="{{ url_for('static', filename='semantic/semantic.min.js') }}"></script> |  | ||||||
|     </body> |     </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -1,29 +1,15 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% macro time_zone_warning() %} |  | ||||||
|     {% trans timezone=event.time_zone, start_time=event.start_time_tz | datetimeformat(rebase=false), end_time=event.end_time_tz | datetimeformat(rebase=false) -%} |  | ||||||
| This event is organised in the {{timezone}} time zone, in which it happens between {{start_time}} and {{end_time}} |  | ||||||
|     {%- endtrans %} |  | ||||||
| {% endmacro %} |  | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h2 class="ui header"> | <h1> | ||||||
|     <div class="content"> |     {{ event.title }}<br> | ||||||
|         {{ event.title }}<br> |     <small> | ||||||
|         <div class="sub header"> |         {{ event.start_time_for_user(current_user) }}–{{ event.end_time_for_user(current_user) }} | ||||||
|     {%- if current_user.timezone | string != event.time_zone -%} |     {% if current_user.timezone | string != event.time_zone %} | ||||||
|             <span title="{{ time_zone_warning() }}"> |         ({{ event.start_time_tz }}–{{ event.end_time_tz }} {{ event.time_zone }}) | ||||||
|                 <i class="fa fa-exclamation-triangle timezone-warning"></i> |  | ||||||
|                 <span class="sr-only">{{ time_zone_warning() }}</span> |  | ||||||
|             </span> |  | ||||||
|     {% endif %} |     {% endif %} | ||||||
|             {{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }} |     </small> | ||||||
|             – | </h1> | ||||||
|             {{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }} |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </h2> |  | ||||||
| {{ event.description }} | {{ event.description }} | ||||||
| <hr> | <hr> | ||||||
| <h2>{% trans %}Invited users{% endtrans %}</h2> | <h2>{% trans %}Invited users{% endtrans %}</h2> | ||||||
| @@ -39,12 +25,13 @@ This event is organised in the {{timezone}} time zone, in which it happens betwe | |||||||
| </ul> | </ul> | ||||||
| <hr> | <hr> | ||||||
| <h2>{% trans %}Invite{% endtrans %}</h2> | <h2>{% trans %}Invite{% endtrans %}</h2> | ||||||
| <form method="post" class="ui form"> | <form method="post"> | ||||||
|     {{ form.hidden_tag() }} |     {{ form.hidden_tag() }} | ||||||
|     <div class="inline fields"> |  | ||||||
|         {{ field(form.invitee, inline=true) }} |  | ||||||
|  |  | ||||||
|         <button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button> |     {{ form.invitee.errors }} | ||||||
|     </div> |     {{ form.invitee.label }} | ||||||
|  |     {{ form.invitee}} | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Invite{% endtrans %}</button> | ||||||
| </form> | </form> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -1,27 +1,43 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h2>Create event</h2> | <form method="post"> | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ form.hidden_tag() }} |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|     {% if form.errors %} |     {{ form.errors }} | ||||||
|         {% for error in form.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|         {% endfor %} |  | ||||||
|     <br> |     <br> | ||||||
|     {% endif %} |  | ||||||
|  |  | ||||||
|     {{ field(form.title) }} |     {{ form.title.errors }} | ||||||
|     {{ field(form.time_zone) }} |     {{ form.title.label }} | ||||||
|     {{ field(form.start_time) }} |     {{ form.title }} | ||||||
|     {{ field(form.end_time) }} |     <br> | ||||||
|     {{ field(form.all_day) }} |  | ||||||
|     {{ field(form.description) }} |  | ||||||
|     {{ field(form.visibility) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> |     {{ form.time_zone.errors }} | ||||||
|     <a href="{{ url_for('hello') }}" class="ui button">Cancel</a> |     {{ form.time_zone.label }} | ||||||
|  |     {{ form.time_zone }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.start_time.errors }} | ||||||
|  |     {{ form.start_time.label }} | ||||||
|  |     {{ form.start_time }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.end_time.errors }} | ||||||
|  |     {{ form.end_time.label }} | ||||||
|  |     {{ form.end_time }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.all_day.errors }} | ||||||
|  |     {{ form.all_day.label }} | ||||||
|  |     {{ form.all_day }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.description.errors }} | ||||||
|  |     {{ form.description.label }} | ||||||
|  |     {{ form.description }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Save{% endtrans %}</button> | ||||||
|  |     <a href="{{ url_for('hello') }}">Cancel</a> | ||||||
| </form> | </form> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								calsocial/templates/first-steps.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								calsocial/templates/first-steps.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <h1>{% trans %}First steps{% endtrans %}</h1> | ||||||
|  | <p> | ||||||
|  |     {% trans %}Welcome to Calendar.social!{% endtrans %} | ||||||
|  | </p> | ||||||
|  | <p> | ||||||
|  |     {% trans %}These are the first steps you should make before you can start using the site.{% endtrans %} | ||||||
|  | </p> | ||||||
|  | <form method="post"> | ||||||
|  |     {{ form.errors }} | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |         {{ form.display_name.errors }} | ||||||
|  |         {{ form.display_name.label }} | ||||||
|  |         {{ form.display_name }}<br> | ||||||
|  |         {{ form.display_name.description }} | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |         {{ form.time_zone.errors }} | ||||||
|  |         {{ form.time_zone.label }} | ||||||
|  |         {{ form.time_zone }}<br> | ||||||
|  |         {{ form.time_zone.description }} | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Save{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock content %} | ||||||
							
								
								
									
										13
									
								
								calsocial/templates/follow-requests.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								calsocial/templates/follow-requests.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <h2>{% trans %}Follow requests{% endtrans %}</h2> | ||||||
|  | <ul> | ||||||
|  |     {% for req in requests %} | ||||||
|  |     <li> | ||||||
|  |         {{ req.follower }} | ||||||
|  |         <a href="{{ url_for('accept_follow', follower_id=req.follower_id) }}">{% trans %}Accept{% endtrans %}</a> | ||||||
|  |     </li> | ||||||
|  |     {% endfor %} | ||||||
|  | </ul> | ||||||
|  | {% endblock content %} | ||||||
| @@ -1,11 +1,9 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| {% trans username=current_user.username -%} | {{ _('Welcome to Calendar.social, %(username)s!', username=current_user.username) }} | ||||||
| Welcome to Calendar.social, {{username}}! |  | ||||||
| {%- endtrans %} |  | ||||||
|  |  | ||||||
| {% include 'month-view.html' %} | {% include 'month-view.html' %} | ||||||
|  |  | ||||||
| <a href="{{ url_for('new_event') }}" class="ui primary button">{% trans %}Add event{% endtrans %}</a> | <a href="{{ url_for('new_event') }}">{% trans %}Add event{% endtrans %}</a> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,26 +0,0 @@ | |||||||
| {# |  | ||||||
|    FIXME: This template should live under security/ if the app templates would override extension |  | ||||||
|           templates… |  | ||||||
| #} |  | ||||||
| {% extends 'base.html' %} |  | ||||||
| {% from '_macros.html' import field %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <h1>{% trans %}Login{% endtrans %}</h1> |  | ||||||
| <form method="post" class="ui form"> |  | ||||||
|     {{ login_user_form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {% if login_user_form.errors %} |  | ||||||
|         {% for error in login_user_form.errors %} |  | ||||||
|     {{ error }} |  | ||||||
|         {% endfor %} |  | ||||||
|     <br> |  | ||||||
|     {% endif %} |  | ||||||
|  |  | ||||||
|     {{ field(login_user_form.email) }} |  | ||||||
|     {{ field(login_user_form.password) }} |  | ||||||
|     {{ field(login_user_form.remember) }} |  | ||||||
|  |  | ||||||
|     <button type="submit" class="ui primary button">{% trans %}Login{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock %} |  | ||||||
| @@ -1,3 +1,69 @@ | |||||||
|  | <style> | ||||||
|  |  table.calendar > * { | ||||||
|  |      font-family: sans; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  table.calendar { | ||||||
|  |      width: 100%; | ||||||
|  |      border-collapse: collapse; | ||||||
|  |      table-layout: fixed; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.month > td { | ||||||
|  |      text-align: center; | ||||||
|  |      font-weight: bold; | ||||||
|  |      border-bottom: 2px solid black; | ||||||
|  |      padding-bottom: .5em; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.month > td > a { | ||||||
|  |      font-weight: normal; | ||||||
|  |      color: black; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.month > td.month-name > a { | ||||||
|  |      font-weight: bold; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.days > td { | ||||||
|  |      text-align: center; | ||||||
|  |      border-bottom: 2px solid black; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.week > td { | ||||||
|  |      height: 3em; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.week > td > span.day-num { | ||||||
|  |      font-weight: bold; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.week > td.other-month > span.day-num { | ||||||
|  |      font-weight: normal; | ||||||
|  |      color: #909090; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.week > td.today { | ||||||
|  |      background-color: #d8d8d8; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.week > td > a.event { | ||||||
|  |      display: block; | ||||||
|  |      color: black; | ||||||
|  |      text-decoration: none; | ||||||
|  |      border: 1px solid green; | ||||||
|  |      background-color: white; | ||||||
|  |      border-radius: 2px; | ||||||
|  |      text-overflow: ellipsis; | ||||||
|  |      white-space: nowrap; | ||||||
|  |      overflow: hidden; | ||||||
|  |  } | ||||||
|  |  | ||||||
|  |  tr.sizer > td { | ||||||
|  |      width: 14.2857%; | ||||||
|  |      height: 0; | ||||||
|  |  } | ||||||
|  | </style> | ||||||
| <table class="calendar"> | <table class="calendar"> | ||||||
|     <thead> |     <thead> | ||||||
|         <tr class="sizer"> |         <tr class="sizer"> | ||||||
| @@ -56,9 +122,7 @@ | |||||||
|                 <span class="day-num">{{ day.day }}</span> |                 <span class="day-num">{{ day.day }}</span> | ||||||
|     {% for event in calendar.day_events(day, user=current_user if user_only else none) %} |     {% for event in calendar.day_events(day, user=current_user if user_only else none) %} | ||||||
|                 <a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event"> |                 <a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event"> | ||||||
|         {% if not event.all_day %} |  | ||||||
|                     {{ event.start_time_for_user(current_user).strftime('%H:%M') }}–{{ event.end_time_for_user(current_user).strftime('%H:%M') }} |                     {{ event.start_time_for_user(current_user).strftime('%H:%M') }}–{{ event.end_time_for_user(current_user).strftime('%H:%M') }} | ||||||
|         {% endif %} |  | ||||||
|                     {{ event.title }} |                     {{ event.title }} | ||||||
|                 </a> |                 </a> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|   | |||||||
| @@ -3,7 +3,5 @@ | |||||||
| {% block content %} | {% block content %} | ||||||
| {% for notif in notifs %} | {% for notif in notifs %} | ||||||
| {{ notif.html }}<br> | {{ notif.html }}<br> | ||||||
| {% else %} |  | ||||||
| {% trans %}Nothing to show.{% endtrans %} |  | ||||||
| {% endfor %} | {% endfor %} | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
| @@ -3,8 +3,7 @@ | |||||||
| {% block content %} | {% block content %} | ||||||
| <h1> | <h1> | ||||||
|     {% if profile.locked %} |     {% if profile.locked %} | ||||||
|     <i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i> |     [locked] | ||||||
|     <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> | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								calsocial/templates/profile-edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								calsocial/templates/profile-edit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | {% extends 'settings-base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | {{ super() }} | ||||||
|  | <h2>{% trans %}Edit profile{% endtrans %}</h2> | ||||||
|  | <form method="post"> | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |     {{ form.errors }} | ||||||
|  |  | ||||||
|  |     {{ form.display_name.errors }} | ||||||
|  |     {{ form.display_name.label }} | ||||||
|  |     {{ form.display_name }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.locked.errors }} | ||||||
|  |     {{ form.locked.label }} | ||||||
|  |     {{ form.locked}} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Save{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock content %} | ||||||
							
								
								
									
										30
									
								
								calsocial/templates/registration.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								calsocial/templates/registration.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <form method="post"> | ||||||
|  |     {{ form.errors }} | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {{ form.username.errors }} | ||||||
|  |     {{ form.username.label }} | ||||||
|  |     {{ form.username }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.email.errors }} | ||||||
|  |     {{ form.email.label }} | ||||||
|  |     {{ form.email }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.password.errors }} | ||||||
|  |     {{ form.password.label }} | ||||||
|  |     {{ form.password }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.password_retype.errors }} | ||||||
|  |     {{ form.password_retype.label }} | ||||||
|  |     {{ form.password_retype }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Register{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										10
									
								
								calsocial/templates/settings-base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								calsocial/templates/settings-base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <nav> | ||||||
|  |     <ul> | ||||||
|  |         <li><a href="{{ url_for('edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a></li> | ||||||
|  |         <li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li> | ||||||
|  |     </ul> | ||||||
|  | </nav> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										20
									
								
								calsocial/templates/user-settings.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								calsocial/templates/user-settings.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | {% extends 'settings-base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | {{ super() }} | ||||||
|  | <h2>{% trans %}Settings{% endtrans %}</h2> | ||||||
|  | <form method="post"> | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {{ form.errors }} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     {{ form.timezone.errors }} | ||||||
|  |     {{ form.timezone.label }} | ||||||
|  |     {{ form.timezone}} | ||||||
|  |     <br> | ||||||
|  |  | ||||||
|  |     <button type="submit">{% trans %}Save{% endtrans %}</button> | ||||||
|  |     <a href="{{ url_for('hello') }}">Cancel</a> | ||||||
|  | </form> | ||||||
|  | {% endblock content %} | ||||||
| @@ -1,84 +1,5 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="ui grid"> | <p>Welcome to Calendar.social.  There will be lot of content here soon!</p> | ||||||
|     <div class="row"> |  | ||||||
|         <div class="four wide column"> |  | ||||||
|     {% if not current_user.is_authenticated %} |  | ||||||
|             <h2>{% trans %}Login{% endtrans %}</h2> |  | ||||||
|             <form class="ui form" method="post" action="{{ url_for('security.login') }}"> |  | ||||||
|                 {{ login_form.hidden_tag() }} |  | ||||||
|                 {{ login_form.email.label }} |  | ||||||
|                 {{ login_form.email }} |  | ||||||
|                 {{ login_form.password.label }} |  | ||||||
|                 {{ login_form.password }} |  | ||||||
|                 <button type="submit" class="fluid ui primary button">{% trans %}Login{% endtrans %}</button> |  | ||||||
|             </form> |  | ||||||
|         {% if config.REGISTRATION_ENABLED %} |  | ||||||
|             <div class="ui horizontal divider"> |  | ||||||
|                 {% trans %}Or{% endtrans %} |  | ||||||
|             </div> |  | ||||||
|             <a href="{{ url_for('account.register_account' ) }}" class="fluid ui button">{% trans %}Register an account{% endtrans %}</a> |  | ||||||
|         {% endif %} |  | ||||||
|             <div class="ui horizontal divider"></div> |  | ||||||
|     {% endif %} |  | ||||||
|             <h2>{% trans %}What is Calendar.social?{% endtrans %}</h2> |  | ||||||
|             <p> |  | ||||||
|                 {% trans %}Calendar.social is a calendar app based on open protocols and free, open source software.{% endtrans %} |  | ||||||
|                 {% trans %}It is decentralised like one of its counterparts, email.{% endtrans %} |  | ||||||
|             </p> |  | ||||||
|     {% if current_user.is_authenticated %} |  | ||||||
|             <div class="ui horizontal divider"></div> |  | ||||||
|             <a href="{{ url_for('hello') }}" class="ui fluid primary button">{% trans %}Go to your calendar{% endtrans %}</a> |  | ||||||
|     {% endif %} |  | ||||||
|         </div> |  | ||||||
|         <div class="twelve wide column"> |  | ||||||
|             <h2>{% trans %}Peek inside{% endtrans %}</h2> |  | ||||||
|     {% include 'month-view.html' %} |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="row"> |  | ||||||
|         <div class="four wide column"> |  | ||||||
|             <h2>{% trans %}Built with users in mind{% endtrans %}</h2> |  | ||||||
|             <p> |  | ||||||
|                 {% trans %}From planning your appointments to organising large scale events Calendar.social can help with all your scheduling needs.{% endtrans %} |  | ||||||
|             </p> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <div class="four wide column"> |  | ||||||
|             <div class="ui centered statistics"> |  | ||||||
|                 <div class="statistic"> |  | ||||||
|                     <div class="value">{{ user_count }}</div> |  | ||||||
|                     <div class="label"> |  | ||||||
|                         {% trans count=user_count %}user{% pluralize %}users{% endtrans %} |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="statistic"> |  | ||||||
|                     <div class="value">{{ event_count }}</div> |  | ||||||
|                     <div class="label"> |  | ||||||
|                         {% trans count=event_count %}event{% pluralize %}events{% endtrans %} |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <div class="four wide column"> |  | ||||||
|             <h2>{% trans %}Built for people{% endtrans %}</h2> |  | ||||||
|             <p> |  | ||||||
|                 {% trans %}Calendar.social is not a commercial network.{% endtrans %} |  | ||||||
|                 {% trans %}No advertising, no data mining, no walled gardens.{% endtrans %} |  | ||||||
|                 {% trans %}There is no central authority.{% endtrans %} |  | ||||||
|             </p> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <div class="four wide column"> |  | ||||||
|             <h2>{% trans %}Administered by{% endtrans %}</h2> |  | ||||||
|             <a href="#" class="ui profile"> |  | ||||||
|                 <div class="avatar"></div> |  | ||||||
|                 <div class="display name">Your Admin here</div> |  | ||||||
|                 <div class="handle">@admin@he.re</div> |  | ||||||
|             </a> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,15 +1,14 @@ | |||||||
| # Hungarian translations for Calendar.social. | # Hungarian translations for PROJECT. | ||||||
| # Copyright (C) 2018 Gergely Polonkai | # Copyright (C) 2018 ORGANIZATION | ||||||
| # This file is distributed under the same license as the Calendar.social | # This file is distributed under the same license as the PROJECT project. | ||||||
| # project. | # FIRST AUTHOR <EMAIL@ADDRESS>, 2018. | ||||||
| # Gergely Polonkai <gergely@polonkai.eu>, 2018. |  | ||||||
| # | # | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version:  0.1\n" | "Project-Id-Version: 0.1\n" | ||||||
| "Report-Msgid-Bugs-To: gergely@polonkai.eu\n" | "Report-Msgid-Bugs-To: gergely@polonkai.eu\n" | ||||||
| "POT-Creation-Date: 2018-07-16 11:09+0200\n" | "POT-Creation-Date: 2018-06-29 14:14+0200\n" | ||||||
| "PO-Revision-Date: 2018-07-16 10:37+0200\n" | "PO-Revision-Date: 2018-06-29 14:26+0200\n" | ||||||
| "Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n" | "Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n" | ||||||
| "Language: hu\n" | "Language: hu\n" | ||||||
| "Language-Team: hu <gergely@polonkai.eu>\n" | "Language-Team: hu <gergely@polonkai.eu>\n" | ||||||
| @@ -19,407 +18,44 @@ msgstr "" | |||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Generated-By: Babel 2.6.0\n" | "Generated-By: Babel 2.6.0\n" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:53 | #: app/forms.py:8 | ||||||
| msgid "This username is not available" |  | ||||||
| msgstr "Ez a felhasználónév nem elérhető" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:84 |  | ||||||
| msgid "This email address can not be used" |  | ||||||
| msgstr "Ez az e-mail cím nem használható" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:96 |  | ||||||
| msgid "Username" | msgid "Username" | ||||||
| msgstr "Felhasználónév" | msgstr "Felhasználónév" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:97 | #: app/forms.py:9 | ||||||
| msgid "Email address" | msgid "Email address" | ||||||
| msgstr "E-mail cím" | msgstr "E-mail cím" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:98 | #: app/forms.py:10 | ||||||
| msgid "Password" | msgid "Password" | ||||||
| msgstr "Jelszó" | msgstr "Jelszó" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:99 | #: app/forms.py:11 | ||||||
| msgid "Password, once more" | msgid "Password, once more" | ||||||
| msgstr "Jelszó még egszer" | msgstr "Jelszó még egszer" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:108 | #: app/forms.py:15 | ||||||
| msgid "The two passwords must match!" | msgid "The two passwords must match!" | ||||||
| msgstr "A két jelszónak egyeznie kell!" | msgstr "A két jelszónak egyeznie kell!" | ||||||
|  |  | ||||||
| #: calsocial/forms.py:176 | #: app/templates/base.html:21 | ||||||
| msgid "Title" |  | ||||||
| msgstr "Cím" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:177 calsocial/forms.py:209 |  | ||||||
| msgid "Time zone" |  | ||||||
| msgstr "Időzóna" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:178 |  | ||||||
| msgid "Start time" |  | ||||||
| msgstr "Kezdés időpontja" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:179 |  | ||||||
| msgid "End time" |  | ||||||
| msgstr "Befejezés időpontja" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:180 |  | ||||||
| msgid "All day" |  | ||||||
| msgstr "Egész napos" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:181 |  | ||||||
| msgid "Description" |  | ||||||
| msgstr "Leírás" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:202 |  | ||||||
| msgid "End time must be later than start time!" |  | ||||||
| msgstr "A befejezés időpontjának későbbre kell esni, mint a kezdés időpontjának!" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:233 |  | ||||||
| msgid "Username or email" |  | ||||||
| msgstr "Felhasználónév vagy e-mail cím" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:321 |  | ||||||
| msgid "User is already invited" |  | ||||||
| msgstr "Ez a felhasználó már meg van hívva" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:331 calsocial/forms.py:345 |  | ||||||
| msgid "Display name" |  | ||||||
| msgstr "Megjelenítendő név" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:334 |  | ||||||
| msgid "" |  | ||||||
| "This will be shown to other users as your name.  You can use your real " |  | ||||||
| "name, or any nickname you like." |  | ||||||
| msgstr "" |  | ||||||
| "Ezt látja majd a többi felhasználó.  Megadhatod a valódi neved, vagy a " |  | ||||||
| "kedvenc beceneved." |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:336 |  | ||||||
| msgid "Your time zone" |  | ||||||
| msgstr "Az időzónád" |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:338 |  | ||||||
| msgid "The start and end times of events will be displayed in this time zone." |  | ||||||
| msgstr "" |  | ||||||
| "Az események kezdési és befejezési időpontjai eszerint az időzóna szerint" |  | ||||||
| " jelennek majd meg." |  | ||||||
|  |  | ||||||
| #: calsocial/forms.py:346 |  | ||||||
| msgid "Lock profile" |  | ||||||
| msgstr "Profil zárolása" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:72 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(actor)s followed you" |  | ||||||
| msgstr "%(actor)s követ téged" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:72 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(actor)s followed %(item)s" |  | ||||||
| msgstr "%(actor)s követi ezt: %(item)s" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:73 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(actor)s invited you to %(item)s" |  | ||||||
| msgstr "%(actor)s meghívott erre: %(item)s" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:499 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(user)s logged in" |  | ||||||
| msgstr "%(user)s bejelentkezett" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:500 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(user)s failed to log in" |  | ||||||
| msgstr "%(user)s nem tudott bejelentkezni" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:501 |  | ||||||
| #, python-format |  | ||||||
| msgid "%(user)s logged out" |  | ||||||
| msgstr "%(user)s kijelentkezett" |  | ||||||
|  |  | ||||||
| #: calsocial/models.py:518 |  | ||||||
| #, python-format |  | ||||||
| msgid "UNKNOWN RECORD \"%(log_type)s\" for %(user)s" |  | ||||||
| msgstr "ISMERETLEN BEJEGYZÉS TÍPUS (\"%(log_type)s\") %(user)s felhasználótól" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:43 |  | ||||||
| msgid "Gregorian" |  | ||||||
| msgstr "Gergely naptár" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:51 |  | ||||||
| msgid "January" |  | ||||||
| msgstr "Január" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:52 |  | ||||||
| msgid "February" |  | ||||||
| msgstr "Február" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:53 |  | ||||||
| msgid "March" |  | ||||||
| msgstr "Március" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:54 |  | ||||||
| msgid "April" |  | ||||||
| msgstr "Április" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:55 |  | ||||||
| msgid "May" |  | ||||||
| msgstr "Május" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:56 |  | ||||||
| msgid "June" |  | ||||||
| msgstr "Június" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:57 |  | ||||||
| msgid "July" |  | ||||||
| msgstr "Július" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:58 |  | ||||||
| msgid "August" |  | ||||||
| msgstr "Augusztus" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:59 |  | ||||||
| msgid "September" |  | ||||||
| msgstr "Szeptember" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:60 |  | ||||||
| msgid "October" |  | ||||||
| msgstr "Október" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:61 |  | ||||||
| msgid "November" |  | ||||||
| msgstr "November" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:62 |  | ||||||
| msgid "December" |  | ||||||
| msgstr "December" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:66 |  | ||||||
| msgid "Monday" |  | ||||||
| msgstr "Hétfő" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:67 |  | ||||||
| msgid "Tuesday" |  | ||||||
| msgstr "Kedd" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:68 |  | ||||||
| msgid "Wednesday" |  | ||||||
| msgstr "Szerda" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:69 |  | ||||||
| msgid "Thursday" |  | ||||||
| msgstr "Csütörtök" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:70 |  | ||||||
| msgid "Friday" |  | ||||||
| msgstr "Péntek" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:71 |  | ||||||
| msgid "Saturday" |  | ||||||
| msgstr "Szombat" |  | ||||||
|  |  | ||||||
| #: calsocial/calendar_system/gregorian.py:72 |  | ||||||
| msgid "Sunday" |  | ||||||
| msgstr "Vasárnap" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:29 calsocial/templates/login.html:9 |  | ||||||
| #: calsocial/templates/login.html:24 calsocial/templates/welcome.html:7 |  | ||||||
| #: calsocial/templates/welcome.html:14 |  | ||||||
| msgid "Login" |  | ||||||
| msgstr "Bejelentkezés" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:32 |  | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Logged in as %(username)s" | msgid "Logged in as %(username)s" | ||||||
| msgstr "Bejelentkezve %(username)s néven" | msgstr "Bejelentkezve %(username)s néven" | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:36 | #: app/templates/base.html:25 | ||||||
| msgid "Calendar view" | msgid "Login" | ||||||
| msgstr "Naptár nézet" | msgstr "Bejelentkezés" | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:37 | #: app/templates/base.html:27 | ||||||
| msgid "Notifications" |  | ||||||
| msgstr "Értesítések" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:38 calsocial/templates/settings-base.html:8 |  | ||||||
| #: calsocial/templates/user-settings.html:5 |  | ||||||
| msgid "Settings" |  | ||||||
| msgstr "Beállítások" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/base.html:39 |  | ||||||
| msgid "Logout" | msgid "Logout" | ||||||
| msgstr "Kijelentkezés" | msgstr "Kijelentkezés" | ||||||
|  |  | ||||||
| #: calsocial/templates/event-details.html:5 | #: app/templates/index.html:4 | ||||||
| #, python-format |  | ||||||
| msgid "" |  | ||||||
| "This event is organised in the %(timezone)s time zone, in which it " |  | ||||||
| "happens between %(start_time)s and %(end_time)s" |  | ||||||
| msgstr "" |  | ||||||
| "Ez az esemény a(z) %(timezone)s időzónában van szervezve, melyben " |  | ||||||
| "%(start_time)s és %(end_time)s között történik" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/event-details.html:29 |  | ||||||
| msgid "Invited users" |  | ||||||
| msgstr "Meghívott felhasználók" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/event-details.html:41 |  | ||||||
| #: calsocial/templates/event-details.html:47 |  | ||||||
| msgid "Invite" |  | ||||||
| msgstr "Meghívás" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/event-edit.html:23 |  | ||||||
| #: calsocial/templates/first-steps.html:25 |  | ||||||
| #: calsocial/templates/profile-edit.html:19 |  | ||||||
| #: calsocial/templates/user-settings.html:18 |  | ||||||
| msgid "Save" |  | ||||||
| msgstr "Mentés" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/first-steps.html:5 |  | ||||||
| msgid "First steps" |  | ||||||
| msgstr "Első lépések" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/first-steps.html:7 |  | ||||||
| msgid "Welcome to Calendar.social!" |  | ||||||
| msgstr "Üdv a Calendar.social-ban!" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/first-steps.html:10 |  | ||||||
| msgid "" |  | ||||||
| "These are the first steps you should make before you can start using the " |  | ||||||
| "site." |  | ||||||
| msgstr "" |  | ||||||
| "Ezek az első lépések melyeket meg kell tenned, mielőtt elkezded használni" |  | ||||||
| " az oldalt." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/follow-requests.html:4 |  | ||||||
| msgid "Follow requests" |  | ||||||
| msgstr "Követési kérések" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/follow-requests.html:10 |  | ||||||
| msgid "Accept" |  | ||||||
| msgstr "Elfogad" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/follow-requests.html:15 |  | ||||||
| msgid "No requests to display." |  | ||||||
| msgstr "Nincs megjeleníthető kérés." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/follow-requests.html:19 |  | ||||||
| msgid "Your profile is not locked." |  | ||||||
| msgstr "A profilod nincs zárolva." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/follow-requests.html:20 |  | ||||||
| msgid "Anyone can follow you without your consent." |  | ||||||
| msgstr "Bárki követhet a beleegyezésed nélkül." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/index.html:4 |  | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Welcome to Calendar.social, %(username)s!" | msgid "Welcome to Calendar.social, %(username)s!" | ||||||
| msgstr "Üdv a Calendar.social-ben, %(username)s!" | msgstr "Üdv a Calendar.social-ben, %(username)s!" | ||||||
|  |  | ||||||
| #: calsocial/templates/index.html:10 | #: app/templates/registration.html:27 | ||||||
| msgid "Add event" |  | ||||||
| msgstr "Esemény létrehozása" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/notifications.html:7 |  | ||||||
| msgid "Nothing to show." |  | ||||||
| msgstr "Nincsenek értesítések." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/profile-details.html:6 |  | ||||||
| #: calsocial/templates/profile-details.html:7 |  | ||||||
| msgid "locked profile" |  | ||||||
| msgstr "zárolt profil" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/profile-details.html:13 |  | ||||||
| msgid "Follow" |  | ||||||
| msgstr "Követés" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/profile-details.html:17 |  | ||||||
| msgid "Follows" |  | ||||||
| msgstr "Követett felhasználók" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/profile-details.html:25 |  | ||||||
| msgid "Followers" |  | ||||||
| msgstr "Követők" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/profile-edit.html:5 |  | ||||||
| #: calsocial/templates/settings-base.html:7 |  | ||||||
| msgid "Edit profile" |  | ||||||
| msgstr "Profil szerkesztése" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/registration.html:17 |  | ||||||
| msgid "Register" | msgid "Register" | ||||||
| msgstr "Regisztráció" | msgstr "Regisztráció" | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:18 |  | ||||||
| msgid "Or" |  | ||||||
| msgstr "Vagy" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:20 |  | ||||||
| msgid "Register an account" |  | ||||||
| msgstr "Regisztrálj egy fiókot" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:23 |  | ||||||
| msgid "What is Calendar.social?" |  | ||||||
| msgstr "Mi az a Calendar.social?" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:25 |  | ||||||
| msgid "" |  | ||||||
| "Calendar.social is a calendar app based on open protocols and free, open " |  | ||||||
| "source software." |  | ||||||
| msgstr "" |  | ||||||
| "A Calendar.social egy naptár alkalmazás, mely nyílt protokollokat és " |  | ||||||
| "szabad, nyílt forráskódú szoftvereket használ." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:26 |  | ||||||
| msgid "It is decentralised like one of its counterparts, email." |  | ||||||
| msgstr "Decentralizált, mint legismertebb társa, az e-mail." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:30 |  | ||||||
| msgid "Peek inside" |  | ||||||
| msgstr "Less be" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:36 |  | ||||||
| msgid "Built for users in mind" |  | ||||||
| msgstr "A felhasználók igényeire szabva" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:38 |  | ||||||
| #, fuzzy |  | ||||||
| msgid "" |  | ||||||
| "From planning your appointments to organising large scale events " |  | ||||||
| "Calendar.social can help with all your scheduling needs." |  | ||||||
| msgstr "" |  | ||||||
| "Találkozók tervezésétől a nagyméretű rendezvények szervezéséig, a " |  | ||||||
| "Calendar.social segít az ütemezésben." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:47 |  | ||||||
| msgid "user" |  | ||||||
| msgid_plural "users" |  | ||||||
| msgstr[0] "felhasználó" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:53 |  | ||||||
| msgid "event" |  | ||||||
| msgid_plural "events" |  | ||||||
| msgstr[0] "esemény" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:60 |  | ||||||
| msgid "Built for people" |  | ||||||
| msgstr "Embereknek készítve" |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:62 |  | ||||||
| msgid "Calendar.social is not a commercial network." |  | ||||||
| msgstr "A Calendar.social nem egy kereskedelmi hálózat." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:63 |  | ||||||
| msgid "No advertising, no data mining, no walled gardens." |  | ||||||
| msgstr "Nincs reklám, nincs adatbányászat, nincsenek korlátok." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:64 |  | ||||||
| msgid "There is no central authority." |  | ||||||
| msgstr "Nincs központi autoritás." |  | ||||||
|  |  | ||||||
| #: calsocial/templates/welcome.html:69 |  | ||||||
| msgid "Administered by" |  | ||||||
| msgstr "Üzemelteti" |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -68,51 +68,3 @@ def force_locale(locale): | |||||||
|         babel.locale_selector_func = orig_locale_selector_func |         babel.locale_selector_func = orig_locale_selector_func | ||||||
|         for key, value in orig_attrs.items(): |         for key, value in orig_attrs.items(): | ||||||
|             setattr(ctx, key, value) |             setattr(ctx, key, value) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RoutedMixin: |  | ||||||
|     """Mixin to lazily register class methods as routes |  | ||||||
|  |  | ||||||
|     Works both for `Flask` and `Blueprint` objects. |  | ||||||
|  |  | ||||||
|     Example:: |  | ||||||
|  |  | ||||||
|     class MyBlueprint(Blueprint, RoutedMixin): |  | ||||||
|         def __init__(self, *args, **kwargs): |  | ||||||
|             do_whatever_you_like() |  | ||||||
|  |  | ||||||
|             RoutedMixin.register_routes(self) |  | ||||||
|  |  | ||||||
|         @RoutedMixin.route('/') |  | ||||||
|         def index(self): |  | ||||||
|             return 'Hello, World!' |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def register_routes(self): |  | ||||||
|         for attr_name in self.__dir__(): |  | ||||||
|             attr = getattr(self, attr_name) |  | ||||||
|  |  | ||||||
|             if not callable(attr): |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             args, kwargs = getattr(attr, 'routing', (None, None)) |  | ||||||
|  |  | ||||||
|             if args is None: |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             self.route(*args, **kwargs)(attr) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def route(*args, **kwargs): |  | ||||||
|         """Mark a function as a future route |  | ||||||
|  |  | ||||||
|         Such functions will be iterated over when the application is initialised.  ``*args`` and |  | ||||||
|         ``**kwargs`` will be passed verbatim to `Flask.route()`. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         def decorator(func):  # pylint: disable=missing-docstring |  | ||||||
|             setattr(func, 'routing', (args, kwargs)) |  | ||||||
|  |  | ||||||
|             return func |  | ||||||
|  |  | ||||||
|         return decorator |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user