forked from gergely/calendar-social
Compare commits
33 Commits
model-upda
...
vagrant
Author | SHA1 | Date | |
---|---|---|---|
60ad2c7ae2 | |||
4935e6394b | |||
2c01939ef5 | |||
e45726fd7c | |||
26d58daac4 | |||
387b7d83ac | |||
9b27491652 | |||
6078e6171f | |||
8eb52ff7f4 | |||
cb9a62cd88 | |||
8d71edae5e | |||
6c98c9d7ca | |||
bcb7b524f3 | |||
8d45611e35 | |||
89dc258a5b | |||
c90b261de3 | |||
372a1f756a | |||
43a90a237f | |||
a763662cd6 | |||
41b4b9d7ea | |||
64c72b1a68 | |||
d36817ca44 | |||
a862e6ca5d | |||
f2f7ef72dd | |||
808c6bbdde | |||
496b638694 | |||
ff304dc64d | |||
13e55e7c68 | |||
b54674c703 | |||
b82cacc665 | |||
d06cfaa02e | |||
a133218906 | |||
0714474dc6 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@ __pycache__/
|
||||
/messages.pot
|
||||
/calsocial/translations/*/LC_MESSAGES/*.mo
|
||||
/.pytest_cache/
|
||||
/.env
|
||||
/.vagrant/
|
||||
/ansible/*.retry
|
||||
|
1
Pipfile
1
Pipfile
@@ -13,6 +13,7 @@ sqlalchemy-utils = "*"
|
||||
bcrypt = "*"
|
||||
flask-babelex = "*"
|
||||
python-dateutil = "*"
|
||||
flask-caching = "*"
|
||||
|
||||
[dev-packages]
|
||||
pylint = "*"
|
||||
|
55
Pipfile.lock
generated
55
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7"
|
||||
"sha256": "e4313bc9baef5cb187176951d45094fe1de4ccba0d15ab58efbac21b6434f255"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -128,6 +128,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.9.3"
|
||||
},
|
||||
"flask-caching": {
|
||||
"hashes": [
|
||||
"sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227",
|
||||
"sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"flask-login": {
|
||||
"hashes": [
|
||||
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
|
||||
@@ -247,9 +255,9 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957"
|
||||
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
|
||||
],
|
||||
"version": "==1.2.9"
|
||||
"version": "==1.2.10"
|
||||
},
|
||||
"sqlalchemy-utils": {
|
||||
"hashes": [
|
||||
@@ -276,10 +284,10 @@
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a",
|
||||
"sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a"
|
||||
"sha256:8704779744963d56a2625ec2949eb150bd499fc099510161ddbb2b64e2d98138",
|
||||
"sha256:add3fd690e7c1fe92436d17be461feeaa173e6f33e0789734310334da0f30027"
|
||||
],
|
||||
"version": "==1.6.5"
|
||||
"version": "==2.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
@@ -369,11 +377,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c",
|
||||
"sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3"
|
||||
"sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4",
|
||||
"sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.2"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
@@ -390,6 +398,35 @@
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
|
||||
|
74
Vagrantfile
vendored
Normal file
74
Vagrantfile
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||
# configures the configuration version (we support older styles for
|
||||
# backwards compatibility). Please don't change it unless you know what
|
||||
# you're doing.
|
||||
Vagrant.configure("2") do |config|
|
||||
# The most common configuration options are documented and commented below.
|
||||
# For a complete reference, please see the online documentation at
|
||||
# https://docs.vagrantup.com.
|
||||
|
||||
# Every Vagrant development environment requires a box. You can search for
|
||||
# boxes at https://vagrantcloud.com/search.
|
||||
config.vm.box = 'fedora/28-cloud-base'
|
||||
|
||||
# Disable automatic box update checking. If you disable this, then
|
||||
# boxes will only be checked for updates when the user runs
|
||||
# `vagrant box outdated`. This is not recommended.
|
||||
# config.vm.box_check_update = false
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||
# NOTE: This will enable public access to the opened port
|
||||
config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine and only allow access
|
||||
# via 127.0.0.1 to disable public access
|
||||
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||
|
||||
# Create a public network, which generally matched to bridged network.
|
||||
# Bridged networks make the machine appear as another physical device on
|
||||
# your network.
|
||||
# config.vm.network "public_network"
|
||||
|
||||
config.vm.synced_folder './', '/vagrant', type: 'sshfs'
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# the path on the host to the actual folder. The second argument is
|
||||
# the path on the guest to mount the folder. And the optional third
|
||||
# argument is a set of non-required options.
|
||||
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||
|
||||
# Provider-specific configuration so you can fine-tune various
|
||||
# backing providers for Vagrant. These expose provider-specific options.
|
||||
# Example for VirtualBox:
|
||||
#
|
||||
# config.vm.provider "virtualbox" do |vb|
|
||||
# # Display the VirtualBox GUI when booting the machine
|
||||
# vb.gui = true
|
||||
#
|
||||
# # Customize the amount of memory on the VM:
|
||||
# vb.memory = "1024"
|
||||
# end
|
||||
#
|
||||
# View the documentation for the provider you are using for more
|
||||
# information on available options.
|
||||
|
||||
# Enable provisioning with a shell script. Additional provisioners such as
|
||||
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
||||
# documentation for more information about their specific syntax and use.
|
||||
config.vm.provision "ansible_local" do |ansible|
|
||||
ansible.compatibility_mode = '2.0'
|
||||
ansible.install = true
|
||||
ansible.provisioning_path = '/vagrant/ansible'
|
||||
ansible.playbook = 'dev.yml'
|
||||
end
|
||||
end
|
23
ansible/dev.yml
Normal file
23
ansible/dev.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
|
||||
- name: Configuration for local development on Vagrant
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
user_name: vagrant
|
||||
group_name: vagrant
|
||||
|
||||
roles:
|
||||
- common
|
||||
- python
|
||||
- role: gunicorn
|
||||
autostart: false
|
||||
enabled: false
|
||||
- role: nginx
|
||||
use_ssl: false
|
||||
enabled: false
|
||||
|
||||
tasks:
|
||||
- name: Allow virtualenv python to bind to port 80
|
||||
command: setcap cap_net_bind_service=ep /usr/bin/python3.6
|
||||
changed_when: false
|
12
ansible/install.sh
Normal file
12
ansible/install.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
if [ ! -f /etc/ansible/hosts ]
|
||||
then
|
||||
echo "Installing Ansible..."
|
||||
sudo dnf remove ansible
|
||||
sudo dnf install ansible-python3
|
||||
|
||||
printf 'localhost\n' | sudo tee /etc/ansible/hosts > /dev/null
|
||||
fi
|
||||
|
||||
echo "Ansible is installed."
|
8
ansible/roles/common/tasks/main.yml
Normal file
8
ansible/roles/common/tasks/main.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
|
||||
- name: Install required packages
|
||||
dnf:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- libselinux-python
|
10
ansible/roles/common/vars/main.yml
Normal file
10
ansible/roles/common/vars/main.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
|
||||
# Project name
|
||||
project_name: calendar.social
|
||||
|
||||
# Project path
|
||||
project_path: /vagrant
|
||||
|
||||
# Flask app path
|
||||
application_path: /vagrant/app
|
56
ansible/roles/gunicorn/tasks/main.yml
Normal file
56
ansible/roles/gunicorn/tasks/main.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
|
||||
- name: Install Supervisor
|
||||
dnf:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- supervisor
|
||||
|
||||
- name: Start supervisord
|
||||
service:
|
||||
name: supervisord
|
||||
state: restarted
|
||||
|
||||
- name: Create the Gunicorn config directory
|
||||
file:
|
||||
path: /etc/gunicorn
|
||||
state: directory
|
||||
owner: "{{ user_name }}"
|
||||
group: "{{ group_name }}"
|
||||
mode: 0700
|
||||
|
||||
- name: Create the Gunicorn config file in /etc/gunicorn
|
||||
template:
|
||||
src: gunicorn.conf.j2
|
||||
dest: /etc/gunicorn/gunicorn.conf
|
||||
|
||||
- name: Create the Gunicorn log directory
|
||||
file:
|
||||
path: /var/log/gunicorn
|
||||
state: directory
|
||||
owner: "{{ user_name }}"
|
||||
group: "{{ group_name }}"
|
||||
mode: 0700
|
||||
|
||||
- name: Create the Supervisor config file for Gunicorn
|
||||
template:
|
||||
src: supervisor.conf.j2
|
||||
dest: /etc/supervisord.d/gunicorn.ini
|
||||
|
||||
- name: Re-read the Supervisor config files
|
||||
supervisorctl:
|
||||
name: gunicorn
|
||||
state: present
|
||||
|
||||
- name: Start Gunicorn with supervisord
|
||||
supervisorctl:
|
||||
name: gunicorn
|
||||
state: restarted
|
||||
when: enabled
|
||||
|
||||
- name: Stop Gunicorn for local dev
|
||||
supervisorctl:
|
||||
name: gunicorn
|
||||
state: stopped
|
||||
when: not enabled
|
9
ansible/roles/gunicorn/templates/gunicorn.conf.j2
Normal file
9
ansible/roles/gunicorn/templates/gunicorn.conf.j2
Normal file
@@ -0,0 +1,9 @@
|
||||
import multiprocessing
|
||||
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
proc_name = 'gunicorn'
|
||||
bind = '127.0.0.1:8000'
|
||||
errorlog = '/var/log/gunicorn/gunicorn-error.log'
|
||||
accesslog = '/var/log/gunicorn/gunicorn-access.log'
|
||||
loglevel = 'warning'
|
||||
timeout = 60
|
8
ansible/roles/gunicorn/templates/supervisor.conf.j2
Normal file
8
ansible/roles/gunicorn/templates/supervisor.conf.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
[program:gunicorn]
|
||||
command=pipenv run gunicorn wsgi:app -c /etc/gunicorn/gunicorn.conf --pythonpath {{ application_path }}
|
||||
directory={{ application_path }}
|
||||
user={{ user_name }}
|
||||
group={{ group_name }}
|
||||
autorestart=true
|
||||
autostart={{ autostart | bool | lower }}
|
||||
redirect_stderr=true
|
11
ansible/roles/nginx/handlers/main.yml
Normal file
11
ansible/roles/nginx/handlers/main.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
|
||||
- name: Reload Nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
|
||||
- name: Stop Nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: stopped
|
42
ansible/roles/nginx/tasks/main.yml
Normal file
42
ansible/roles/nginx/tasks/main.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
|
||||
- name: Install Nginx
|
||||
dnf:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- nginx
|
||||
|
||||
- name: Create the Nginx configuration file for SSL
|
||||
template:
|
||||
src: site-ssl.conf.j2
|
||||
dest: /etc/nginx/conf.d/{{ project_name }}-ssl.conf
|
||||
when: use_ssl
|
||||
notify: Reload Nginx
|
||||
|
||||
- name: Create the Nginx configuration file (non-SSL)
|
||||
template:
|
||||
src: site.conf.j2
|
||||
dest: /etc/nginx/conf.d/{{ project_name }}.conf
|
||||
when: not use_ssl
|
||||
notify: Reload Nginx
|
||||
|
||||
- name: Ensure that the default site is removed
|
||||
file:
|
||||
path: /etc/nginx/conf.d/default.conf
|
||||
state: absent
|
||||
|
||||
- name: Ensure Nginx service is started, enable service on restart
|
||||
service:
|
||||
name: nginx
|
||||
state: restarted
|
||||
enabled: yes
|
||||
when: enabled
|
||||
|
||||
- name: Stop nginx for local dev, disable service
|
||||
service:
|
||||
name: nginx
|
||||
state: stopped
|
||||
enabled: no
|
||||
notify: Stop Nginx
|
||||
when: not enabled
|
41
ansible/roles/nginx/templates/site-ssl.conf.j2
Normal file
41
ansible/roles/nginx/templates/site-ssl.conf.j2
Normal file
@@ -0,0 +1,41 @@
|
||||
upstream appserver {
|
||||
server localhost:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl deferred;
|
||||
server_name {{ host_name }};
|
||||
|
||||
ssl_certificate {{ home_path }}/{{ project_name }}.crt;
|
||||
ssl_certificate_key {{ home_path }}/{{ project_name }}.key;
|
||||
ssl_session_cache shared:SSL:32m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
access_log /var/log/nginx/{{ project_name }}.access.log;
|
||||
error_log /var/log/nginx/{{ project_name }}.error.log info;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
||||
location /static {
|
||||
alias {{ project_path }}/static;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
proxy_read_timeout 180s;
|
||||
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://appserver;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
29
ansible/roles/nginx/templates/site.conf.j2
Normal file
29
ansible/roles/nginx/templates/site.conf.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
upstream appserver {
|
||||
server localhost:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ host_name }};
|
||||
|
||||
access_log /var/log/nginx/{{ project_name }}.access.log;
|
||||
error_log /var/log/nginx/{{ project_name }}.error.log info;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
||||
location /static {
|
||||
alias {{ project_path }}/static;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
proxy_read_timeout 180s;
|
||||
|
||||
if (-f $request_filename) {
|
||||
proxy_pass http://appserver;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
3
ansible/roles/nginx/vars/main.yml
Normal file
3
ansible/roles/nginx/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
|
||||
host_name: calendar-social.local
|
22
ansible/roles/python/tasks/main.yml
Normal file
22
ansible/roles/python/tasks/main.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
|
||||
- name: Install common python packages
|
||||
dnf:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- pipenv
|
||||
|
||||
- name: Delete Python cache files
|
||||
command: find . -type d -name __pycache__ -exec rm -r {} +
|
||||
args:
|
||||
chdir: "{{ project_path }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Install packages
|
||||
command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy
|
||||
|
||||
- name: Install development related packages
|
||||
command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy --dev
|
||||
args:
|
||||
chdir: "{{ project_path }}"
|
@@ -25,6 +25,10 @@ from flask_babelex import Babel, get_locale as babel_get_locale
|
||||
from flask_security import SQLAlchemyUserDatastore, current_user, login_required
|
||||
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
|
||||
|
||||
from calsocial.account import AccountBlueprint
|
||||
from calsocial.cache import CachedSessionInterface, cache
|
||||
from calsocial.utils import RoutedMixin
|
||||
|
||||
|
||||
def get_locale():
|
||||
"""Locale selector
|
||||
@@ -53,22 +57,7 @@ def template_vars():
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
class CalendarSocialApp(Flask, RoutedMixin):
|
||||
"""The Calendar.social app
|
||||
"""
|
||||
|
||||
@@ -79,13 +68,22 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
Flask.__init__(self, name)
|
||||
|
||||
self.session_interface = CachedSessionInterface()
|
||||
|
||||
self._timezone = None
|
||||
|
||||
config_name = os.environ.get('ENV', config or 'dev')
|
||||
config_name = os.environ.get('ENV', config or 'development')
|
||||
self.config.from_pyfile(f'config_{config_name}.py', True)
|
||||
# Make sure we look up users both by their usernames and email addresses
|
||||
self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email')
|
||||
self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html'
|
||||
|
||||
self.jinja_env.policies['ext.i18n.trimmed'] = True # pylint: disable=no-member
|
||||
|
||||
db.init_app(self)
|
||||
|
||||
cache.init_app(self)
|
||||
|
||||
babel = Babel(app=self)
|
||||
babel.localeselector(get_locale)
|
||||
|
||||
@@ -96,18 +94,9 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
self.context_processor(template_vars)
|
||||
|
||||
for attr_name in self.__dir__():
|
||||
attr = getattr(self, attr_name)
|
||||
RoutedMixin.register_routes(self)
|
||||
|
||||
if not callable(attr):
|
||||
continue
|
||||
|
||||
args, kwargs = getattr(attr, 'routing', (None, None))
|
||||
|
||||
if args is None:
|
||||
continue
|
||||
|
||||
self.route(*args, **kwargs)(attr)
|
||||
AccountBlueprint().init_app(self, '/accounts/')
|
||||
|
||||
self.before_request(self.goto_first_steps)
|
||||
|
||||
@@ -119,7 +108,7 @@ class CalendarSocialApp(Flask):
|
||||
if current_user.is_authenticated and \
|
||||
not current_user.profile and \
|
||||
request.endpoint != 'first_steps':
|
||||
return redirect(url_for('first_steps'))
|
||||
return redirect(url_for('account.first_steps'))
|
||||
|
||||
return None
|
||||
|
||||
@@ -150,60 +139,58 @@ class CalendarSocialApp(Flask):
|
||||
return self._timezone
|
||||
|
||||
@staticmethod
|
||||
@route('/')
|
||||
def hello():
|
||||
"""View for the main page
|
||||
|
||||
This will display a welcome message for users not logged in; for others, their main
|
||||
calendar view is displayed.
|
||||
"""
|
||||
|
||||
def _current_calendar():
|
||||
from .calendar_system.gregorian import GregorianCalendar
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('welcome.html')
|
||||
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
|
||||
except TypeError:
|
||||
timestamp = datetime.utcnow()
|
||||
|
||||
calendar = GregorianCalendar(timestamp.timestamp())
|
||||
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
|
||||
|
||||
This will display a welcome message for users not logged in; for others, their main
|
||||
calendar view is displayed.
|
||||
"""
|
||||
|
||||
calendar = self._current_calendar()
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return self.about()
|
||||
|
||||
return render_template('index.html', calendar=calendar, user_only=True)
|
||||
|
||||
@staticmethod
|
||||
@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'])
|
||||
@RoutedMixin.route('/new-event', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_event():
|
||||
"""View for creating a new event
|
||||
@@ -228,28 +215,7 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('event-edit.html', form=form)
|
||||
|
||||
@staticmethod
|
||||
@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'])
|
||||
@RoutedMixin.route('/event/<string:event_uuid>', methods=['GET', 'POST'])
|
||||
def event_details(event_uuid):
|
||||
"""View to display event details
|
||||
"""
|
||||
@@ -276,7 +242,7 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('event-details.html', event=event, form=form)
|
||||
|
||||
@staticmethod
|
||||
@route('/profile/@<string:username>')
|
||||
@RoutedMixin.route('/profile/@<string:username>')
|
||||
def display_profile(username):
|
||||
"""View to display profile details
|
||||
"""
|
||||
@@ -291,7 +257,7 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('profile-details.html', profile=profile)
|
||||
|
||||
@staticmethod
|
||||
@route('/profile/@<string:username>/follow')
|
||||
@RoutedMixin.route('/profile/@<string:username>/follow')
|
||||
@login_required
|
||||
def follow_user(username):
|
||||
"""View for following a user
|
||||
@@ -312,22 +278,7 @@ class CalendarSocialApp(Flask):
|
||||
return redirect(url_for('display_profile', username=username))
|
||||
|
||||
@staticmethod
|
||||
@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>')
|
||||
@RoutedMixin.route('/accept/<int:invite_id>')
|
||||
def accept_invite(invite_id):
|
||||
"""View to accept an invitation
|
||||
"""
|
||||
@@ -357,55 +308,7 @@ class CalendarSocialApp(Flask):
|
||||
return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid))
|
||||
|
||||
@staticmethod
|
||||
@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')
|
||||
@RoutedMixin.route('/all-events')
|
||||
def all_events():
|
||||
"""View for listing all available events
|
||||
"""
|
||||
@@ -421,45 +324,5 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
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__)
|
||||
|
@@ -6,4 +6,4 @@ from calsocial import CalendarSocialApp
|
||||
|
||||
app = CalendarSocialApp('calsocial')
|
||||
|
||||
app.run()
|
||||
app.run(host='0.0.0.0', port=80)
|
||||
|
234
calsocial/account.py
Normal file
234
calsocial/account.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# 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'))
|
153
calsocial/cache.py
Normal file
153
calsocial/cache.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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,17 +83,23 @@ class GregorianCalendar(CalendarSystem):
|
||||
def days(self):
|
||||
day_list = []
|
||||
|
||||
start_day = self.timestamp.replace(day=1)
|
||||
month_first = self.timestamp.replace(day=1)
|
||||
|
||||
while start_day.weekday() > self.START_DAY:
|
||||
start_day -= timedelta(days=1)
|
||||
if self.timestamp.month == 12:
|
||||
month_last = month_first.replace(day=31)
|
||||
else:
|
||||
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
|
||||
|
||||
day_list.append(start_day)
|
||||
current_day = start_day
|
||||
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7
|
||||
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7
|
||||
|
||||
while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
|
||||
current_day += timedelta(days=1)
|
||||
day_list.append(current_day)
|
||||
first_display = month_first - timedelta(days=pad_before)
|
||||
last_display = month_last + timedelta(days=pad_after)
|
||||
current = first_display
|
||||
|
||||
while current <= last_display:
|
||||
day_list.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
return day_list
|
||||
|
||||
@@ -199,21 +205,29 @@ class GregorianCalendar(CalendarSystem):
|
||||
"""Returns all events for a given day
|
||||
"""
|
||||
|
||||
from ..models import Event, Profile
|
||||
from ..models import Event, EventVisibility, Invitation, Profile, Response
|
||||
|
||||
events = Event.query
|
||||
|
||||
if user:
|
||||
events = events.join(Profile) \
|
||||
events = events.outerjoin(Invitation) \
|
||||
.outerjoin(Response) \
|
||||
.join(Profile, Event.profile) \
|
||||
.filter(Profile.user == user)
|
||||
|
||||
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_timestamp = start_timestamp + timedelta(days=1)
|
||||
|
||||
events = events.filter(((Event.start_time >= start_timestamp) &
|
||||
(Event.start_time < end_timestamp)) |
|
||||
((Event.end_time >= start_timestamp) &
|
||||
(Event.end_time < end_timestamp))) \
|
||||
events = events.filter((Event.start_time <= end_timestamp) &
|
||||
(Event.end_time >= start_timestamp)) \
|
||||
.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
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Configuration file for the development environment
|
||||
"""
|
||||
|
||||
ENV = 'dev'
|
||||
ENV = 'development'
|
||||
#: If ``True``, registration on the site is enabled.
|
||||
REGISTRATION_ENABLED = True
|
||||
#: The default time zone
|
||||
@@ -14,3 +14,4 @@ SECRET_KEY = 'ThisIsNotSoSecret'
|
||||
SECURITY_PASSWORD_HASH = 'bcrypt'
|
||||
SECURITY_PASSWORD_SALT = SECRET_KEY
|
||||
SECURITY_REGISTERABLE = False
|
||||
CACHE_TYPE = 'simple'
|
@@ -17,6 +17,8 @@
|
||||
"""Forms for Calendar.social
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from flask_babelex import lazy_gettext as _
|
||||
from flask_security.forms import LoginForm as BaseLoginForm
|
||||
from flask_wtf import FlaskForm
|
||||
@@ -26,6 +28,8 @@ from wtforms.ext.dateutil.fields import DateTimeField
|
||||
from wtforms.validators import DataRequired, Email, StopValidation, ValidationError
|
||||
from wtforms.widgets import TextArea
|
||||
|
||||
from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS
|
||||
|
||||
|
||||
class UsernameAvailable(object): # pylint: disable=too-few-public-methods
|
||||
"""Checks if a username is available
|
||||
@@ -169,6 +173,45 @@ class TimezoneField(SelectField):
|
||||
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):
|
||||
"""Form for event creation/editing
|
||||
"""
|
||||
@@ -179,6 +222,14 @@ class EventForm(FlaskForm):
|
||||
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
|
||||
all_day = BooleanField(_('All day'))
|
||||
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):
|
||||
"""Populate ``obj`` with event data
|
||||
|
@@ -27,6 +27,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy_utils.types.choice import ChoiceType
|
||||
|
||||
from .cache import cache
|
||||
from .utils import force_locale
|
||||
|
||||
db = SQLAlchemy()
|
||||
@@ -103,6 +104,23 @@ class ResponseType(Enum):
|
||||
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:
|
||||
"""Proxy object to get settings for a user
|
||||
"""
|
||||
@@ -202,6 +220,24 @@ class User(db.Model, UserMixin):
|
||||
|
||||
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):
|
||||
return f'<User {self.id}({self.username})>'
|
||||
|
||||
@@ -399,6 +435,9 @@ class Event(db.Model):
|
||||
#: The description of the event
|
||||
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):
|
||||
from pytz import timezone, utc
|
||||
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"""Security related things for Calendar.social
|
||||
"""
|
||||
|
||||
from flask import current_app
|
||||
from flask import current_app, session
|
||||
from flask_login.signals import user_logged_in, user_logged_out
|
||||
from flask_security import Security, AnonymousUser as BaseAnonymousUser
|
||||
|
||||
@@ -45,6 +45,8 @@ def login_handler(app, user): # pylint: disable=unused-argument
|
||||
|
||||
AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS)
|
||||
|
||||
user.active_sessions += [session.sid]
|
||||
|
||||
|
||||
@user_logged_out.connect
|
||||
def logout_handler(app, user): # pylint: disable=unused-argument
|
||||
|
121
calsocial/static/css/style.css
Normal file
121
calsocial/static/css/style.css
Normal file
@@ -0,0 +1,121 @@
|
||||
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
Normal file
364
calsocial/static/semantic/semantic.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
calsocial/static/semantic/semantic.min.js
vendored
Normal file
11
calsocial/static/semantic/semantic.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
calsocial/templates/_macros.html
Normal file
20
calsocial/templates/_macros.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% 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 %}
|
15
calsocial/templates/account/active-sessions.html
Normal file
15
calsocial/templates/account/active-sessions.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% 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 %}
|
27
calsocial/templates/account/first-steps.html
Normal file
27
calsocial/templates/account/first-steps.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% 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 %}
|
23
calsocial/templates/account/follow-requests.html
Normal file
23
calsocial/templates/account/follow-requests.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% 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 %}
|
@@ -3,5 +3,7 @@
|
||||
{% block content %}
|
||||
{% for notif in notifs %}
|
||||
{{ notif.html }}<br>
|
||||
{% else %}
|
||||
{% trans %}Nothing to show.{% endtrans %}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
21
calsocial/templates/account/profile-edit.html
Normal file
21
calsocial/templates/account/profile-edit.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% 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 %}
|
19
calsocial/templates/account/registration.html
Normal file
19
calsocial/templates/account/registration.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% 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 %}
|
16
calsocial/templates/account/settings-base.html
Normal file
16
calsocial/templates/account/settings-base.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% 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 %}
|
20
calsocial/templates/account/user-settings.html
Normal file
20
calsocial/templates/account/user-settings.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% 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,47 +7,49 @@
|
||||
<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-192.png') }}">
|
||||
{% block head %}
|
||||
<style>
|
||||
header > h1 > img {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3em;
|
||||
font-weight: bold;
|
||||
border-top: 1px dotted black;
|
||||
padding-top: 1em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
|
||||
Calendar.social
|
||||
</h1>
|
||||
<nav class="menu">
|
||||
{% if 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) }}
|
||||
{% endif %}
|
||||
<ul>
|
||||
<div class="ui top attached menu">
|
||||
<div class="header item">
|
||||
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
|
||||
Calendar.social
|
||||
</div>
|
||||
<div class="right menu">
|
||||
{% if not current_user.is_authenticated %}
|
||||
<li><a href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a></li>
|
||||
<a class="item" href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a>
|
||||
{% 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>
|
||||
<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 %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui container" id="content">
|
||||
{% block content %}{% endblock %}
|
||||
<footer>
|
||||
</div>
|
||||
<footer class="ui segment">
|
||||
<a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br>
|
||||
Soon…™
|
||||
</footer>
|
||||
{% 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>
|
||||
</html>
|
||||
|
@@ -1,15 +1,29 @@
|
||||
{% 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 %}
|
||||
<h1>
|
||||
{{ event.title }}<br>
|
||||
<small>
|
||||
{{ event.start_time_for_user(current_user) }}–{{ event.end_time_for_user(current_user) }}
|
||||
{% if current_user.timezone | string != event.time_zone %}
|
||||
({{ event.start_time_tz }}–{{ event.end_time_tz }} {{ event.time_zone }})
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
{{ event.title }}<br>
|
||||
<div class="sub header">
|
||||
{%- if current_user.timezone | string != event.time_zone -%}
|
||||
<span title="{{ time_zone_warning() }}">
|
||||
<i class="fa fa-exclamation-triangle timezone-warning"></i>
|
||||
<span class="sr-only">{{ time_zone_warning() }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
</h1>
|
||||
{{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }}
|
||||
–
|
||||
{{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }}
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{{ event.description }}
|
||||
<hr>
|
||||
<h2>{% trans %}Invited users{% endtrans %}</h2>
|
||||
@@ -25,13 +39,12 @@
|
||||
</ul>
|
||||
<hr>
|
||||
<h2>{% trans %}Invite{% endtrans %}</h2>
|
||||
<form method="post">
|
||||
<form method="post" class="ui form">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="inline fields">
|
||||
{{ field(form.invitee, inline=true) }}
|
||||
|
||||
{{ form.invitee.errors }}
|
||||
{{ form.invitee.label }}
|
||||
{{ form.invitee}}
|
||||
|
||||
<button type="submit">{% trans %}Invite{% endtrans %}</button>
|
||||
<button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@@ -1,43 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from '_macros.html' import field %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<h2>Create event</h2>
|
||||
<form method="post" class="ui form">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ form.errors }}
|
||||
{% if form.errors %}
|
||||
{% for error in form.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{{ form.title.errors }}
|
||||
{{ form.title.label }}
|
||||
{{ form.title }}
|
||||
<br>
|
||||
{{ field(form.title) }}
|
||||
{{ field(form.time_zone) }}
|
||||
{{ field(form.start_time) }}
|
||||
{{ field(form.end_time) }}
|
||||
{{ field(form.all_day) }}
|
||||
{{ field(form.description) }}
|
||||
{{ field(form.visibility) }}
|
||||
|
||||
{{ form.time_zone.errors }}
|
||||
{{ 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>
|
||||
<button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button>
|
||||
<a href="{{ url_for('hello') }}" class="ui button">Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
{% 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 %}
|
@@ -1,13 +0,0 @@
|
||||
{% 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,9 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{{ _('Welcome to Calendar.social, %(username)s!', username=current_user.username) }}
|
||||
{% trans username=current_user.username -%}
|
||||
Welcome to Calendar.social, {{username}}!
|
||||
{%- endtrans %}
|
||||
|
||||
{% include 'month-view.html' %}
|
||||
|
||||
<a href="{{ url_for('new_event') }}">{% trans %}Add event{% endtrans %}</a>
|
||||
<a href="{{ url_for('new_event') }}" class="ui primary button">{% trans %}Add event{% endtrans %}</a>
|
||||
{% endblock content %}
|
||||
|
26
calsocial/templates/login.html
Normal file
26
calsocial/templates/login.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
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,69 +1,3 @@
|
||||
<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">
|
||||
<thead>
|
||||
<tr class="sizer">
|
||||
@@ -122,7 +56,9 @@
|
||||
<span class="day-num">{{ day.day }}</span>
|
||||
{% 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">
|
||||
{% 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') }}
|
||||
{% endif %}
|
||||
{{ event.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@@ -3,7 +3,8 @@
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% if profile.locked %}
|
||||
[locked]
|
||||
<i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i>
|
||||
<span class="sr-only">{% trans %}locked profile{% endtrans %}</span>
|
||||
{% endif %}
|
||||
{{ profile.display_name }}
|
||||
<small>@{{ profile.user.username}}</small>
|
||||
|
@@ -1,22 +0,0 @@
|
||||
{% 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 %}
|
@@ -1,30 +0,0 @@
|
||||
{% 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 %}
|
@@ -1,10 +0,0 @@
|
||||
{% 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 %}
|
@@ -1,20 +0,0 @@
|
||||
{% 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,5 +1,84 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<p>Welcome to Calendar.social. There will be lot of content here soon!</p>
|
||||
<div class="ui grid">
|
||||
<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 %}
|
||||
|
@@ -1,14 +1,15 @@
|
||||
# Hungarian translations for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
# Hungarian translations for Calendar.social.
|
||||
# Copyright (C) 2018 Gergely Polonkai
|
||||
# This file is distributed under the same license as the Calendar.social
|
||||
# project.
|
||||
# Gergely Polonkai <gergely@polonkai.eu>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1\n"
|
||||
"Project-Id-Version: 0.1\n"
|
||||
"Report-Msgid-Bugs-To: gergely@polonkai.eu\n"
|
||||
"POT-Creation-Date: 2018-06-29 14:14+0200\n"
|
||||
"PO-Revision-Date: 2018-06-29 14:26+0200\n"
|
||||
"POT-Creation-Date: 2018-07-16 11:09+0200\n"
|
||||
"PO-Revision-Date: 2018-07-16 10:37+0200\n"
|
||||
"Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n"
|
||||
"Language: hu\n"
|
||||
"Language-Team: hu <gergely@polonkai.eu>\n"
|
||||
@@ -18,44 +19,407 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.6.0\n"
|
||||
|
||||
#: app/forms.py:8
|
||||
#: calsocial/forms.py:53
|
||||
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"
|
||||
msgstr "Felhasználónév"
|
||||
|
||||
#: app/forms.py:9
|
||||
#: calsocial/forms.py:97
|
||||
msgid "Email address"
|
||||
msgstr "E-mail cím"
|
||||
|
||||
#: app/forms.py:10
|
||||
#: calsocial/forms.py:98
|
||||
msgid "Password"
|
||||
msgstr "Jelszó"
|
||||
|
||||
#: app/forms.py:11
|
||||
#: calsocial/forms.py:99
|
||||
msgid "Password, once more"
|
||||
msgstr "Jelszó még egszer"
|
||||
|
||||
#: app/forms.py:15
|
||||
#: calsocial/forms.py:108
|
||||
msgid "The two passwords must match!"
|
||||
msgstr "A két jelszónak egyeznie kell!"
|
||||
|
||||
#: app/templates/base.html:21
|
||||
#: calsocial/forms.py:176
|
||||
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
|
||||
msgid "Logged in as %(username)s"
|
||||
msgstr "Bejelentkezve %(username)s néven"
|
||||
|
||||
#: app/templates/base.html:25
|
||||
msgid "Login"
|
||||
msgstr "Bejelentkezés"
|
||||
#: calsocial/templates/base.html:36
|
||||
msgid "Calendar view"
|
||||
msgstr "Naptár nézet"
|
||||
|
||||
#: app/templates/base.html:27
|
||||
#: calsocial/templates/base.html:37
|
||||
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"
|
||||
msgstr "Kijelentkezés"
|
||||
|
||||
#: app/templates/index.html:4
|
||||
#: calsocial/templates/event-details.html:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This event is organised in the %(timezone)s time zone, in which it "
|
||||
"happens between %(start_time)s and %(end_time)s"
|
||||
msgstr ""
|
||||
"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
|
||||
msgid "Welcome to Calendar.social, %(username)s!"
|
||||
msgstr "Üdv a Calendar.social-ben, %(username)s!"
|
||||
|
||||
#: app/templates/registration.html:27
|
||||
#: calsocial/templates/index.html:10
|
||||
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"
|
||||
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,3 +68,51 @@ def force_locale(locale):
|
||||
babel.locale_selector_func = orig_locale_selector_func
|
||||
for key, value in orig_attrs.items():
|
||||
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