33 Commits

Author SHA1 Message Date
60ad2c7ae2 Add Vagrant related files 2018-07-23 11:59:13 +02:00
4935e6394b [Development] Ignore the .env file
It is used by pipenv, and we definitely don’t want to accidentally commit this to the repo.
2018-07-23 11:53:56 +02:00
2c01939ef5 [Docs] Add docstring to User.active_sessions 2018-07-23 11:53:56 +02:00
e45726fd7c [Refactor] Make the session list a list comprehension in account.py 2018-07-23 11:53:56 +02:00
26d58daac4 [Code Cleanup] Wrap a long line in accounts.py 2018-07-23 11:53:56 +02:00
387b7d83ac [Bugfix] Fix the environment name for dev mode
It turns out Flask only considers `development` as the development mode; `dev` (used before) is
not working.
2018-07-23 11:53:56 +02:00
9b27491652 [Bugfix] Import gettext in account.py 2018-07-23 11:53:43 +02:00
6078e6171f [Bugfix] Rework month padding and event fetching routines
Month padding (ie. adding the days of previous/next months) is now working as expected.  Fetching
multi-day events now also displays events correctly.
2018-07-23 08:00:06 +02:00
8eb52ff7f4 Hide time values for all-day events 2018-07-21 06:50:17 +02:00
cb9a62cd88 Make it possible to list and invalidate active sessions 2018-07-19 15:15:49 +02:00
8d71edae5e Save sessions in the cache 2018-07-19 15:15:49 +02:00
6c98c9d7ca Add caching functionality via Flask-Caching 2018-07-19 15:15:49 +02:00
bcb7b524f3 Move account related views to a separate blueprint 2018-07-19 15:15:49 +02:00
8d45611e35 Create the RoutedMixin class
It will be used both in the app, and later blueprint classes.
2018-07-17 15:00:56 +02:00
89dc258a5b [Bugfix] Fix the link of the “Go to your calendar” button on the about page 2018-07-17 12:39:09 +02:00
c90b261de3 [Refactor] Refactor the about page
This makes it available to logged in users, too.
2018-07-17 10:06:46 +02:00
372a1f756a Update the welcome page with actual numbers 2018-07-17 09:52:50 +02:00
43a90a237f Document the EnumField form field type 2018-07-16 13:34:05 +02:00
a763662cd6 Make sure the EnumField gets an Enum subclass as its parameter 2018-07-16 13:34:05 +02:00
41b4b9d7ea Fix the label of the EnumField field type
If there are no translations provided, use the enum name
2018-07-16 13:34:05 +02:00
64c72b1a68 Make PyLint happy 2018-07-16 13:34:05 +02:00
d36817ca44 Make the time zone on the event creation form default to the user’s time zone 2018-07-16 12:37:32 +02:00
a862e6ca5d Add the Event.visibility field
This shows if the event is visible to anyone or just people who are invited.

The calendar view already respects this flag.
2018-07-16 12:12:35 +02:00
f2f7ef72dd Update Hungarian translations 2018-07-16 11:12:56 +02:00
808c6bbdde Update translatable strings
Stop using the `_()` function, and use `{% trans %}` tags instead.
2018-07-16 11:09:09 +02:00
496b638694 [Bugfix] Add a missing closing tag to event-details.html 2018-07-16 10:42:10 +02:00
ff304dc64d Create the contents of the welcome page 2018-07-15 07:31:49 +02:00
13e55e7c68 [Cleanup] Style fix in style.css 2018-07-15 07:18:50 +02:00
b54674c703 Semantic UI version 2018-07-13 16:03:33 +02:00
b82cacc665 Make locked profiles display a lock icon instead of the text “locked”
This involves adding [ForkAwesome](https://forkawesome.github.io/) to the dependencies, even
though it is pulled in via a CDN.
2018-07-13 15:33:08 +02:00
d06cfaa02e Add a field macro to the event creation form 2018-07-13 15:33:06 +02:00
a133218906 Add necessary META tags
Charset and viewport
2018-07-13 09:34:56 +02:00
0714474dc6 Move site CSS to a separate file 2018-07-13 08:58:12 +02:00
54 changed files with 2253 additions and 519 deletions

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@ __pycache__/
/messages.pot
/calsocial/translations/*/LC_MESSAGES/*.mo
/.pytest_cache/
/.env
/.vagrant/
/ansible/*.retry

View File

@@ -13,6 +13,7 @@ sqlalchemy-utils = "*"
bcrypt = "*"
flask-babelex = "*"
python-dateutil = "*"
flask-caching = "*"
[dev-packages]
pylint = "*"

55
Pipfile.lock generated
View File

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

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

23
ansible/dev.yml Normal file
View File

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

12
ansible/install.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 registrants 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 ones 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__)

View File

@@ -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
View 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 registrants 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 ones 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(_('Cant 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
View 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

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

View File

@@ -3,5 +3,7 @@
{% block content %}
{% for notif in notifs %}
{{ notif.html }}<br>
{% else %}
{% trans %}Nothing to show.{% endtrans %}
{% endfor %}
{% endblock content %}

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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