Compare commits

..

1 Commits

Author SHA1 Message Date
86f86996f6 Add Drone configuration
Some checks failed
the build failed
2018-07-11 11:08:16 +02:00
56 changed files with 581 additions and 4121 deletions

17
.drone.yml Normal file
View File

@ -0,0 +1,17 @@
pipeline:
build:
image: python:3.6.6-alpine3.6
commands:
- apk update
- apk add gcc libffi-dev linux-headers libc-dev
- pip3.6 install -U pip setuptools pipenv
- pipenv install
lint:
image: python:3.6.6-alpine3.6
commands:
- apk update
- apk add gcc libffi-dev linux-headers libc-dev
- pip3.6 install -U pip setuptools pipenv
- pipenv install
- pipenv install --dev
- pipenv run pylint calsocial

View File

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

5
.gitignore vendored
View File

@ -1,8 +1,5 @@
__pycache__/ __pycache__/
/calsocial/local.db /calsocial/local.db
/messages.pot /messages.pot
/calsocial/translations/*/LC_MESSAGES/*.mo /app/translations/*/LC_MESSAGES/*.mo
/.pytest_cache/ /.pytest_cache/
/.env
/.coverage
/htmlcov/

View File

@ -13,12 +13,10 @@ sqlalchemy-utils = "*"
bcrypt = "*" bcrypt = "*"
flask-babelex = "*" flask-babelex = "*"
python-dateutil = "*" python-dateutil = "*"
flask-caching = "*"
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"
pytest = "*" pytest = "*"
pytest-cov = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

107
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294" "sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -128,14 +128,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.9.3" "version": "==0.9.3"
}, },
"flask-caching": {
"hashes": [
"sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227",
"sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985"
],
"index": "pypi",
"version": "==1.4.0"
},
"flask-login": { "flask-login": {
"hashes": [ "hashes": [
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
@ -255,9 +247,9 @@
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8" "sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957"
], ],
"version": "==1.2.10" "version": "==1.2.9"
}, },
"sqlalchemy-utils": { "sqlalchemy-utils": {
"hashes": [ "hashes": [
@ -284,10 +276,10 @@
"develop": { "develop": {
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a"
], ],
"version": "==2.0.1" "version": "==1.6.5"
}, },
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
@ -303,50 +295,6 @@
], ],
"version": "==18.1.0" "version": "==18.1.0"
}, },
"coverage": {
"hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
],
"version": "==4.5.1"
},
"isort": { "isort": {
"hashes": [ "hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
@ -421,11 +369,11 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.1" "version": "==1.9.2"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
@ -435,14 +383,6 @@
"index": "pypi", "index": "pypi",
"version": "==3.6.3" "version": "==3.6.3"
}, },
"pytest-cov": {
"hashes": [
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
],
"index": "pypi",
"version": "==2.5.1"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
@ -450,35 +390,6 @@
], ],
"version": "==1.11.0" "version": "==1.11.0"
}, },
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"

View File

@ -19,18 +19,12 @@
from datetime import datetime from datetime import datetime
import os import os
from warnings import warn
from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \ from flask import Flask, abort, current_app, redirect, render_template, request, url_for
url_for
from flask_babelex import Babel, get_locale as babel_get_locale from flask_babelex import Babel, get_locale as babel_get_locale
from flask_security import SQLAlchemyUserDatastore, current_user, login_required from flask_security import SQLAlchemyUserDatastore, current_user, login_required
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from calsocial.account import AccountBlueprint
from calsocial.cache import CachedSessionInterface, cache
from calsocial.utils import RoutedMixin
def get_locale(): def get_locale():
"""Locale selector """Locale selector
@ -59,7 +53,22 @@ def template_vars():
} }
class CalendarSocialApp(Flask, RoutedMixin): def route(*args, **kwargs):
"""Mark a function as a future route
Such functions will be iterated over when the application is initialised. ``*args`` and
``**kwargs`` will be passed verbatim to `Flask.route()`.
"""
def decorator(func): # pylint: disable=missing-docstring
setattr(func, 'routing', (args, kwargs))
return func
return decorator
class CalendarSocialApp(Flask):
"""The Calendar.social app """The Calendar.social app
""" """
@ -70,32 +79,13 @@ class CalendarSocialApp(Flask, RoutedMixin):
Flask.__init__(self, name) Flask.__init__(self, name)
self.session_interface = CachedSessionInterface()
self._timezone = None self._timezone = None
config_name = os.environ.get('FLASK_ENV', config or 'development') config_name = os.environ.get('ENV', config or 'dev')
self.config.from_pyfile(f'config_{config_name}.py', True) self.config.from_pyfile(f'config_{config_name}.py', True)
# Make sure we look up users both by their usernames and email addresses # Make sure we look up users both by their usernames and email addresses
self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email')
self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html'
# The builtin avatars to use
self.config['BUILTIN_AVATARS'] = (
'doctor',
'engineer',
'scientist',
'statistician',
'user',
'whoami',
)
self.jinja_env.policies['ext.i18n.trimmed'] = True # pylint: disable=no-member
db.init_app(self) db.init_app(self)
cache.init_app(self)
babel = Babel(app=self) babel = Babel(app=self)
babel.localeselector(get_locale) babel.localeselector(get_locale)
@ -106,9 +96,18 @@ class CalendarSocialApp(Flask, RoutedMixin):
self.context_processor(template_vars) self.context_processor(template_vars)
RoutedMixin.register_routes(self) for attr_name in self.__dir__():
attr = getattr(self, attr_name)
AccountBlueprint().init_app(self, '/accounts/') if not callable(attr):
continue
args, kwargs = getattr(attr, 'routing', (None, None))
if args is None:
continue
self.route(*args, **kwargs)(attr)
self.before_request(self.goto_first_steps) self.before_request(self.goto_first_steps)
@ -119,8 +118,8 @@ class CalendarSocialApp(Flask, RoutedMixin):
if current_user.is_authenticated and \ if current_user.is_authenticated and \
not current_user.profile and \ not current_user.profile and \
request.endpoint != 'account.first_steps': request.endpoint != 'first_steps':
return redirect(url_for('account.first_steps')) return redirect(url_for('first_steps'))
return None return None
@ -129,6 +128,9 @@ class CalendarSocialApp(Flask, RoutedMixin):
"""The default time zone of the app """The default time zone of the app
""" """
from warnings import warn
from flask import has_app_context
from pytz import timezone, utc from pytz import timezone, utc
from pytz.exceptions import UnknownTimeZoneError from pytz.exceptions import UnknownTimeZoneError
@ -147,88 +149,61 @@ class CalendarSocialApp(Flask, RoutedMixin):
return self._timezone return self._timezone
@property
def instance_admin(self):
"""The admin user of this instance
"""
from calsocial.models import AppState, User
if not has_app_context():
return None
admin_id = AppState['instance_admin']
try:
admin_id = int(admin_id)
except (TypeError, ValueError):
warn(f'Instance admin is not set correctly (value is {admin_id})')
return None
try:
return User.query.filter(User.id == admin_id).one()
except NoResultFound:
warn(f'Instance admin is not set correctly (value is {admin_id})')
return None
@staticmethod @staticmethod
def _current_calendar(): @route('/')
from .calendar_system.gregorian import GregorianCalendar def hello():
try:
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
except TypeError:
timestamp = datetime.utcnow()
return GregorianCalendar(timestamp.timestamp())
@RoutedMixin.route('/about')
def about(self):
"""View for the about page
"""
from .models import User, Event
calendar = self._current_calendar()
if not current_user.is_authenticated:
login_form_class = current_app.extensions['security'].login_form
login_form = login_form_class()
else:
login_form = None
user_count = User.query.count()
event_count = Event.query.count()
admin_user = current_app.instance_admin
admin_profile = None if admin_user is None else admin_user.profile
return render_template('welcome.html',
calendar=calendar,
user_only=False,
login_form=login_form,
user_count=user_count,
event_count=event_count,
admin_profile=admin_profile)
@RoutedMixin.route('/')
def hello(self):
"""View for the main page """View for the main page
This will display a welcome message for users not logged in; for others, their main This will display a welcome message for users not logged in; for others, their main
calendar view is displayed. calendar view is displayed.
""" """
calendar = self._current_calendar() from .calendar_system.gregorian import GregorianCalendar
if not current_user.is_authenticated: if not current_user.is_authenticated:
return self.about() return render_template('welcome.html')
return render_template('index.html', calendar=calendar, user_only=True) try:
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
except TypeError:
timestamp = datetime.utcnow()
calendar = GregorianCalendar(timestamp.timestamp())
return render_template('index.html', calendar=calendar)
@staticmethod @staticmethod
@RoutedMixin.route('/new-event', methods=['GET', 'POST']) @route('/register', methods=['POST', 'GET'])
def register():
"""View for user registration
If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the
registration disabled template. Otherwise, it performs user registration.
"""
if not current_app.config['REGISTRATION_ENABLED']:
return render_template('registration-disabled.html')
from .forms import RegistrationForm
from .models import db, User
form = RegistrationForm()
if form.validate_on_submit():
# TODO: This might become False later, if we want registrations to be confirmed via
# e-mail
user = User(active=True)
form.populate_obj(user)
db.session.add(user)
db.session.commit()
return redirect(url_for('hello'))
return render_template('registration.html', form=form)
@staticmethod
@route('/new-event', methods=['GET', 'POST'])
@login_required @login_required
def new_event(): def new_event():
"""View for creating a new event """View for creating a new event
@ -242,7 +217,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
form = EventForm() form = EventForm()
if form.validate_on_submit(): if form.validate_on_submit():
event = Event(profile=current_user.profile) event = Event(user=current_user)
form.populate_obj(event) form.populate_obj(event)
db.session.add(event) db.session.add(event)
@ -253,13 +228,34 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('event-edit.html', form=form) return render_template('event-edit.html', form=form)
@staticmethod @staticmethod
@RoutedMixin.route('/event/<string:event_uuid>', methods=['GET', 'POST']) @route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
"""View for user settings
"""
from .forms import SettingsForm
from .models import db
form = SettingsForm(current_user)
if form.validate_on_submit():
form.populate_obj(current_user)
db.session.commit()
return redirect(url_for('hello'))
return render_template('user-settings.html', form=form)
@staticmethod
@route('/event/<string:event_uuid>', methods=['GET', 'POST'])
def event_details(event_uuid): def event_details(event_uuid):
"""View to display event details """View to display event details
""" """
from .forms import InviteForm from .forms import InviteForm
from .models import db, Event from .models import db, Event, Invitation, Notification, NotificationAction
try: try:
event = Event.query.filter(Event.event_uuid == event_uuid).one() event = Event.query.filter(Event.event_uuid == event_uuid).one()
@ -271,7 +267,15 @@ class CalendarSocialApp(Flask, RoutedMixin):
form = InviteForm(event) form = InviteForm(event)
if form.validate_on_submit(): if form.validate_on_submit():
event.invite(current_user.profile, invitee=form.invitee.data) invite = Invitation(event=event, sender=current_user.profile)
form.populate_obj(invite)
db.session.add(invite)
notification = Notification(profile=form.invitee.data,
actor=current_user.profile,
item=event,
action=NotificationAction.invite)
db.session.add(notification)
db.session.commit() db.session.commit()
@ -280,7 +284,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('event-details.html', event=event, form=form) return render_template('event-details.html', event=event, form=form)
@staticmethod @staticmethod
@RoutedMixin.route('/profile/@<string:username>') @route('/profile/@<string:username>')
def display_profile(username): def display_profile(username):
"""View to display profile details """View to display profile details
""" """
@ -295,13 +299,12 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('profile-details.html', profile=profile) return render_template('profile-details.html', profile=profile)
@staticmethod @staticmethod
@RoutedMixin.route('/profile/@<string:username>/follow') @route('/profile/@<string:username>/follow')
@login_required
def follow_user(username): def follow_user(username):
"""View for following a user """View for following a user
""" """
from .models import db, Profile, User from .models import db, Profile, User, UserFollow, Notification, NotificationAction
try: try:
profile = Profile.query.join(User).filter(User.username == username).one() profile = Profile.query.join(User).filter(User.username == username).one()
@ -309,14 +312,38 @@ class CalendarSocialApp(Flask, RoutedMixin):
abort(404) abort(404)
if profile.user != current_user: if profile.user != current_user:
profile.follow(follower=current_user.profile) follow = UserFollow(follower=current_user.profile,
followed=profile,
accepted_at=datetime.utcnow())
db.session.add(follow)
notification = Notification(profile=profile,
actor=current_user.profile,
item=profile,
action=NotificationAction.follow)
db.session.add(notification)
db.session.commit() db.session.commit()
return redirect(url_for('display_profile', username=username)) return redirect(url_for('display_profile', username=username))
@staticmethod @staticmethod
@RoutedMixin.route('/accept/<int:invite_id>') @route('/notifications')
def notifications():
"""View to list the notifications for the current user
"""
from .models import Notification
if current_user.is_authenticated:
notifs = Notification.query.filter(Notification.profile == current_user.profile)
else:
notifs = []
return render_template('notifications.html', notifs=notifs)
@staticmethod
@route('/accept/<int:invite_id>')
def accept_invite(invite_id): def accept_invite(invite_id):
"""View to accept an invitation """View to accept an invitation
""" """
@ -346,21 +373,31 @@ class CalendarSocialApp(Flask, RoutedMixin):
return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid))
@staticmethod @staticmethod
@RoutedMixin.route('/all-events') @route('/first-steps', methods=['GET', 'POST'])
def all_events(): @login_required
"""View for listing all available events def first_steps():
"""View to set up a new registrants profile
""" """
from .calendar_system.gregorian import GregorianCalendar from .forms import FirstStepsForm
from .models import db, Profile
try: if current_user.profile:
timestamp = datetime.fromtimestamp(float(request.args.get('date'))) return redirect(url_for('hello'))
except TypeError:
timestamp = datetime.utcnow()
calendar = GregorianCalendar(timestamp.timestamp()) form = FirstStepsForm()
return render_template('index.html', calendar=calendar, user_only=False) 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)
app = CalendarSocialApp(__name__) app = CalendarSocialApp(__name__)

View File

@ -1,234 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Main module for the Calendar.social app
"""
from flask import Blueprint, abort, current_app, flash, redirect, render_template, session, url_for
from flask_babelex import gettext as _
from flask_security import current_user, login_required
from sqlalchemy.orm.exc import NoResultFound
from calsocial.utils import RoutedMixin
class AccountBlueprint(Blueprint, RoutedMixin):
"""Blueprint for account management
"""
def __init__(self):
Blueprint.__init__(self, 'account', __name__)
self.app = None
RoutedMixin.register_routes(self)
def init_app(self, app, url_prefix=None):
"""Initialise the blueprint, registering it with ``app``.
"""
self.app = app
app.register_blueprint(self, url_prefix=url_prefix)
@staticmethod
@RoutedMixin.route('/register', methods=['POST', 'GET'])
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'))

View File

@ -1,60 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Metaclass for storing and accessing app state
"""
def get_state_base(self, key):
"""Method to get a key from the state store
"""
return self.__get_state__(key)
def set_state_base(self, key, value):
"""Method to set a key/value in the state store
"""
self.__set_state__(key, str(value))
def set_default_base(self, key, value):
"""Method to set the default value of a key in the state store
If key is already in the state store, this method is a no-op.
"""
self.__set_state_default__(key, str(value))
def app_state_base(klass):
"""Base class creator for AppStateMeta types
:param klass: the class to extend
:type klass: type
:returns: a new class extending ``klass``
:rtype: type
"""
# Construct the meta class based on the metaclass of ``klass``
metaclass = type(
klass.__name__ + 'BaseMeta',
(type(klass),),
{
'__getitem__': get_state_base,
'__setitem__': set_state_base,
'setdefault': set_default_base,
})
return metaclass(klass.__name__ + 'Base', (klass,), {'__abstract__': True})

View File

@ -1,159 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Caching functionality for Calendar.social
"""
from datetime import timedelta
import pickle
from uuid import uuid4
from flask import has_request_context, request as flask_request, session as flask_session
from flask.sessions import SessionInterface, SessionMixin
from flask_caching import Cache
from werkzeug.datastructures import CallbackDict
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'] = flask_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):
"""The user this session belongs to
"""
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, sess):
"""Get the expiration time of the cache entry
"""
if sess.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):
"""Delete the session with ``sid`` as its session ID
"""
if has_request_context() and flask_session.sid == sid:
raise ValueError('Will not delete the current session')
cache.delete(self.prefix + sid)

View File

@ -18,12 +18,24 @@
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import wraps
from flask_babelex import lazy_gettext as _ from flask_babelex import lazy_gettext as _
from . import CalendarSystem from . import CalendarSystem
def to_timestamp(func):
"""Decorator that converts the return value of a function from `datetime` to a UNIX timestamp
"""
@wraps(func)
def _decorator(*args, **kwargs):
return func(*args, **kwargs).timestamp()
return _decorator
class GregorianCalendar(CalendarSystem): class GregorianCalendar(CalendarSystem):
"""Gregorian calendar system for Calendar.social """Gregorian calendar system for Calendar.social
""" """
@ -71,27 +83,22 @@ class GregorianCalendar(CalendarSystem):
def days(self): def days(self):
day_list = [] day_list = []
month_first = self.timestamp.replace(day=1) start_day = self.timestamp.replace(day=1)
if self.timestamp.month == 12: while start_day.weekday() > self.START_DAY:
month_last = month_first.replace(day=31) start_day -= timedelta(days=1)
else:
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7 day_list.append(start_day)
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7 current_day = start_day
first_display = month_first - timedelta(days=pad_before) while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
last_display = month_last + timedelta(days=pad_after) current_day += timedelta(days=1)
current = first_display day_list.append(current_day)
while current <= last_display:
day_list.append(current)
current += timedelta(days=1)
return day_list return day_list
@property @property
@to_timestamp
def prev_year(self): def prev_year(self):
"""Returns the timestamp of the same date in the previous year """Returns the timestamp of the same date in the previous year
""" """
@ -106,6 +113,7 @@ class GregorianCalendar(CalendarSystem):
return self.timestamp.replace(year=self.timestamp.year - 1).year return self.timestamp.replace(year=self.timestamp.year - 1).year
@property @property
@to_timestamp
def prev_month(self): def prev_month(self):
"""Returns the timestamp of the same day in the previous month """Returns the timestamp of the same day in the previous month
""" """
@ -128,6 +136,7 @@ class GregorianCalendar(CalendarSystem):
return self.month_names[timestamp.month - 1] return self.month_names[timestamp.month - 1]
@property @property
@to_timestamp
def next_month(self): def next_month(self):
"""Returns the timestamp of the same day in the next month """Returns the timestamp of the same day in the next month
""" """
@ -150,6 +159,7 @@ class GregorianCalendar(CalendarSystem):
return self.month_names[timestamp.month - 1] return self.month_names[timestamp.month - 1]
@property @property
@to_timestamp
def next_year(self): def next_year(self):
"""Returns the timestamp of the same date in the next year """Returns the timestamp of the same date in the next year
""" """
@ -182,36 +192,28 @@ class GregorianCalendar(CalendarSystem):
month_end_timestamp = month_start_timestamp.replace(month=next_month) month_end_timestamp = month_start_timestamp.replace(month=next_month)
return month_start_timestamp <= now < month_end_timestamp return now >= month_start_timestamp and now < month_end_timestamp
@staticmethod @staticmethod
def day_events(date, user=None): def day_events(date, user=None):
"""Returns all events for a given day """Returns all events for a given day
""" """
from ..models import Event, EventVisibility, Invitation, Profile, Response from ..models import Event, Profile
events = Event.query events = Event.query
if user: if user:
events = events.outerjoin(Invitation) \ events = events.join(Profile) \
.outerjoin(Response) \
.join(Profile, Event.profile) \
.filter(Profile.user == user) .filter(Profile.user == user)
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
end_timestamp = start_timestamp + timedelta(days=1) end_timestamp = start_timestamp + timedelta(days=1)
events = events.filter((Event.start_time <= end_timestamp) & events = events.filter(((Event.start_time >= start_timestamp) &
(Event.end_time >= start_timestamp)) \ (Event.start_time < end_timestamp)) |
((Event.end_time >= start_timestamp) &
(Event.end_time < end_timestamp))) \
.order_by('start_time', 'end_time') .order_by('start_time', 'end_time')
if user is None:
events = events.filter(Event.visibility == EventVisibility.public)
else:
events = events.filter((Event.visibility == EventVisibility.public) |
(Event.profile == user.profile) |
(Invitation.invitee == user.profile) |
(Response.profile == user.profile))
return events return events

View File

@ -1,7 +1,7 @@
"""Configuration file for the development environment """Configuration file for the development environment
""" """
ENV = 'development' ENV = 'dev'
#: If ``True``, registration on the site is enabled. #: If ``True``, registration on the site is enabled.
REGISTRATION_ENABLED = True REGISTRATION_ENABLED = True
#: The default time zone #: The default time zone
@ -14,4 +14,3 @@ SECRET_KEY = 'ThisIsNotSoSecret'
SECURITY_PASSWORD_HASH = 'bcrypt' SECURITY_PASSWORD_HASH = 'bcrypt'
SECURITY_PASSWORD_SALT = SECRET_KEY SECURITY_PASSWORD_SALT = SECRET_KEY
SECURITY_REGISTERABLE = False SECURITY_REGISTERABLE = False
CACHE_TYPE = 'simple'

View File

@ -1,18 +0,0 @@
"""Configuration file for the development environment
"""
ENV = 'testing'
#: If ``True``, registration on the site is enabled.
REGISTRATION_ENABLED = True
#: The default time zone
DEFAULT_TIMEZONE = 'Europe/Budapest'
DEBUG = False
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'WeAreTesting'
SECURITY_PASSWORD_HASH = 'bcrypt'
SECURITY_PASSWORD_SALT = SECRET_KEY
SECURITY_REGISTERABLE = False
CACHE_TYPE = 'simple'

View File

@ -17,21 +17,17 @@
"""Forms for Calendar.social """Forms for Calendar.social
""" """
from enum import Enum
from flask_babelex import lazy_gettext as _ from flask_babelex import lazy_gettext as _
from flask_security.forms import LoginForm as BaseLoginForm from flask_security.forms import LoginForm as BaseLoginForm
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import pytz import pytz
from wtforms import BooleanField, PasswordField, SelectField, StringField, RadioField from wtforms import BooleanField, PasswordField, SelectField, StringField
from wtforms.ext.dateutil.fields import DateTimeField from wtforms.ext.dateutil.fields import DateTimeField
from wtforms.validators import DataRequired, Email, StopValidation, ValidationError from wtforms.validators import DataRequired, Email, StopValidation, ValidationError
from wtforms.widgets import TextArea from wtforms.widgets import TextArea
from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS
class UsernameAvailable(object): # pylint: disable=too-few-public-methods
class UsernameAvailable: # pylint: disable=too-few-public-methods
"""Checks if a username is available """Checks if a username is available
""" """
@ -62,7 +58,7 @@ class UsernameAvailable: # pylint: disable=too-few-public-methods
raise StopValidation(message) raise StopValidation(message)
class EmailAvailable: # pylint: disable=too-few-public-methods class EmailAvailable(object): # pylint: disable=too-few-public-methods
"""Checks if an email address is available """Checks if an email address is available
""" """
@ -173,45 +169,6 @@ class TimezoneField(SelectField):
yield (value, label, value == self.data) yield (value, label, value == self.data)
class EnumField(SelectField):
"""Field that allows selecting one value from an ``Enum`` class
:param enum_type: an ``Enum`` type
:type enum_type: type(Enum)
:param translations: translatable labels for enum values
:type translations: dict
:param args: passed verbatim to the constructor of `SelectField`
:param kwargs: passed verbatim to the constructor of `SelectField`
"""
def __init__(self, enum_type, translations, *args, **kwargs):
if not issubclass(enum_type, Enum):
raise TypeError('enum_type must be a subclass of Enum')
kwargs.update({'choices': [(value, None) for value in enum_type]})
self.data = None
self.enum_type = enum_type
self.translations = translations
SelectField.__init__(self, *args, **kwargs)
def process_formdata(self, valuelist):
if not valuelist:
self.data = None
return
try:
self.data = self.enum_type[valuelist[0]]
except KeyError:
raise ValueError('Unknown value')
def iter_choices(self):
for value in self.enum_type:
label = self.gettext(self.translations[value]) if self.translations else value.name
yield (value.name, label, value == self.data)
class EventForm(FlaskForm): class EventForm(FlaskForm):
"""Form for event creation/editing """Form for event creation/editing
""" """
@ -222,14 +179,6 @@ class EventForm(FlaskForm):
end_time = DateTimeField(_('End time'), validators=[DataRequired()]) end_time = DateTimeField(_('End time'), validators=[DataRequired()])
all_day = BooleanField(_('All day')) all_day = BooleanField(_('All day'))
description = StringField(_('Description'), widget=TextArea()) description = StringField(_('Description'), widget=TextArea())
visibility = EnumField(EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, label=_('Visibility'))
def __init__(self, *args, **kwargs):
from flask_security import current_user
self.time_zone.kwargs['default'] = current_user.timezone # pylint: disable=no-member
FlaskForm.__init__(self, *args, **kwargs)
def populate_obj(self, obj): def populate_obj(self, obj):
"""Populate ``obj`` with event data """Populate ``obj`` with event data
@ -381,33 +330,8 @@ class FirstStepsForm(FlaskForm):
display_name = StringField( display_name = StringField(
label=_('Display name'), label=_('Display name'),
validators=[DataRequired()], validators=[DataRequired()],
# pylint: disable=line-too-long
description=_('This will be shown to other users as your name. You can use your real name, or any nickname you like.')) description=_('This will be shown to other users as your name. You can use your real name, or any nickname you like.'))
time_zone = TimezoneField( time_zone = TimezoneField(
label=_('Your time zone'), label=_('Your time zone'),
validators=[DataRequired()], validators=[DataRequired()],
description=_('The start and end times of events will be displayed in this time zone.')) description=_('The start and end times of events will be displayed in this time zone.'))
class ProfileForm(FlaskForm):
"""Form for editing a user profile
"""
display_name = StringField(label=_('Display name'), validators=[DataRequired()])
builtin_avatar = RadioField(label=_('Use a built-in avatar'))
locked = BooleanField(label=_('Lock profile'))
def __init__(self, profile, *args, **kwargs):
from flask import current_app
kwargs.update(
{
'display_name': profile.display_name,
'locked': profile.locked,
'builtin_avatar': profile.builtin_avatar,
})
FlaskForm.__init__(self, *args, **kwargs)
self.builtin_avatar.choices = [(name, name)
for name in current_app.config['BUILTIN_AVATARS']]
self.profile = profile

View File

@ -21,15 +21,12 @@ from datetime import datetime
from enum import Enum from enum import Enum
from warnings import warn from warnings import warn
from flask import current_app
from flask_babelex import lazy_gettext from flask_babelex import lazy_gettext
from flask_security import UserMixin, RoleMixin from flask_security import UserMixin, RoleMixin
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy_utils.types.choice import ChoiceType from sqlalchemy_utils.types.choice import ChoiceType
from .app_state import app_state_base
from .cache import cache
from .utils import force_locale from .utils import force_locale
db = SQLAlchemy() db = SQLAlchemy()
@ -101,28 +98,11 @@ class ResponseType(Enum):
return self.name.lower() == other.lower() # pylint: disable=no-member return self.name.lower() == other.lower() # pylint: disable=no-member
if isinstance(other, (int, float)): if isinstance(other, (int, float)):
return self.value == other # pylint: disable=comparison-with-callable return self.value == other
return Enum.__eq__(self, other) return Enum.__eq__(self, other)
class EventVisibility(Enum):
"""Enumeration for event visibility
"""
#: The event is private, only attendees and people invited can see the details
private = 0
#: The event is public, anyone can see the details
public = 5
EVENT_VISIBILITY_TRANSLATIONS = {
EventVisibility.private: _('Visible only to attendees'),
EventVisibility.public: _('Visible to everyone'),
}
class SettingsProxy: class SettingsProxy:
"""Proxy object to get settings for a user """Proxy object to get settings for a user
""" """
@ -208,6 +188,7 @@ class User(db.Model, UserMixin):
If the user didnt set a time zone yet, the application default is used. If the user didnt set a time zone yet, the application default is used.
""" """
from flask import current_app
from pytz import timezone from pytz import timezone
from pytz.exceptions import UnknownTimeZoneError from pytz.exceptions import UnknownTimeZoneError
@ -221,24 +202,6 @@ class User(db.Model, UserMixin):
return current_app.timezone return current_app.timezone
@property
def session_list_key(self):
"""The cache key of this 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): def __repr__(self):
return f'<User {self.id}({self.username})>' return f'<User {self.id}({self.username})>'
@ -283,12 +246,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
#: The display name #: The display name
display_name = db.Column(db.Unicode(length=80), nullable=False) display_name = db.Column(db.Unicode(length=80), nullable=False)
#: If locked, a profile cannot be followed without the owners consent
locked = db.Column(db.Boolean(), default=False)
#: If set, the profile will display this builtin avatar
builtin_avatar = db.Column(db.String(length=40), nullable=True)
@property @property
def fqn(self): def fqn(self):
"""The fully qualified name of the profile """The fully qualified name of the profile
@ -308,7 +265,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
domain = '' domain = ''
else: else:
username = self.username username = self.username
domain = f'@{self.domain}' domain = '@' + self.domain
return f'<Profile {self.id}(@{username}{domain})>' return f'<Profile {self.id}(@{username}{domain})>'
@ -333,8 +290,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \ return Profile.query \
.join(UserFollow.followed) \ .join(UserFollow.followed) \
.filter(UserFollow.follower == self) \ .filter(UserFollow.follower == self)
.filter(UserFollow.accepted_at.isnot(None))
@property @property
def follower_list(self): def follower_list(self):
@ -347,8 +303,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \ return Profile.query \
.join(UserFollow.follower) \ .join(UserFollow.follower) \
.filter(UserFollow.followed == self) \ .filter(UserFollow.followed == self)
.filter(UserFollow.accepted_at.isnot(None))
@property @property
def url(self): def url(self):
@ -362,48 +317,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return NotImplemented return NotImplemented
def follow(self, follower):
"""Make ``follower`` follow this profile
"""
if not isinstance(follower, Profile):
raise TypeError('Folloer must be a Profile object')
timestamp = None if self.locked else datetime.utcnow()
user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp)
db.session.add(user_follow)
notification = self.notify(follower, self, NotificationAction.follow)
db.session.add(notification)
return user_follow
def notify(self, actor, item, action):
"""Notify this profile about ``action`` on ``item`` by ``actor``
:param actor: the actor who generated the notification
:type actor: Profile
:param item: the item ``action`` was performed on
:type item: any
:param action: the type of the action
:type action: NotificationAction, str
:raises TypeError: if ``actor`` is not a `Profile` object
:returns: the generated notification. It is already added to the database session, but
not committed
:rtype: Notification
"""
if not isinstance(actor, Profile):
raise TypeError('actor must be a Profile instance')
if isinstance(action, str):
action = NotificationAction[action]
notification = Notification(profile=self, actor=actor, item=item, action=action)
return notification
class Event(db.Model): class Event(db.Model):
"""Database model for events """Database model for events
@ -439,9 +352,6 @@ class Event(db.Model):
#: The description of the event #: The description of the event
description = db.Column(db.UnicodeText()) description = db.Column(db.UnicodeText())
#: The visibility of the event
visibility = db.Column(db.Enum(EventVisibility), nullable=False)
def __as_tz(self, timestamp, as_timezone=None): def __as_tz(self, timestamp, as_timezone=None):
from pytz import timezone, utc from pytz import timezone, utc
@ -490,20 +400,6 @@ class Event(db.Model):
return url_for('event_details', event_uuid=self.event_uuid) return url_for('event_details', event_uuid=self.event_uuid)
def invite(self, inviter, invited):
"""Invite ``invited`` to the event
The invitation will arrive from ``inviter``.
"""
invite = Invitation(event=self, sender=inviter, invitee=invited)
db.session.add(invite)
notification = invited.notify(inviter, self, NotificationAction.invite)
db.session.add(notification)
return invite
class UserSetting(db.Model): # pylint: disable=too-few-public-methods class UserSetting(db.Model): # pylint: disable=too-few-public-methods
"""Database model for user settings """Database model for user settings
@ -647,12 +543,6 @@ class UserFollow(db.Model): # pylint: disable=too-few-public-methods
#: The timestamp when the follow was accepted #: The timestamp when the follow was accepted
accepted_at = db.Column(db.DateTime(), nullable=True) accepted_at = db.Column(db.DateTime(), nullable=True)
def accept(self):
"""Accept this follow request
"""
self.accepted_at = datetime.utcnow()
class Notification(db.Model): class Notification(db.Model):
"""Database model for notifications """Database model for notifications
@ -790,64 +680,3 @@ class Response(db.Model): # pylint: disable=too-few-public-methods
#: The response itself #: The response itself
response = db.Column(db.Enum(ResponseType), nullable=False) response = db.Column(db.Enum(ResponseType), nullable=False)
class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-methods,inherit-non-class
"""Database model for application state values
"""
__tablename__ = 'app_state'
#: The environment that set this key
env = db.Column(db.String(length=40), nullable=False, primary_key=True)
#: The key
key = db.Column(db.String(length=80), nullable=False, primary_key=True)
#: The value of the key
value = db.Column(db.Unicode(length=200), nullable=True)
@classmethod
def __get_state__(cls, key):
try:
record = cls.query \
.filter(cls.env == current_app.env) \
.filter(cls.key == key) \
.one()
except NoResultFound:
return None
return record.value
@classmethod
def __set_state__(cls, key, value):
try:
record = cls.query \
.filter(cls.env == current_app.env) \
.filter(cls.key == key) \
.one()
except NoResultFound:
record = cls(env=current_app.env, key=key)
record.value = value
db.session.add(record)
db.session.commit()
@classmethod
def __set_state_default__(cls, key, value):
try:
record = cls.query \
.filter(cls.env == current_app.env) \
.filter(cls.key == key) \
.one()
except NoResultFound:
pass
else:
return
record = cls(env=current_app.env, key=key, value=value)
db.session.add(record)
db.session.commit()
def __repr__(self):
return f'<AppState {self.env}:{self.key}="{self.value}"'

View File

@ -17,7 +17,7 @@
"""Security related things for Calendar.social """Security related things for Calendar.social
""" """
from flask import current_app, session from flask import current_app
from flask_login.signals import user_logged_in, user_logged_out from flask_login.signals import user_logged_in, user_logged_out
from flask_security import Security, AnonymousUser as BaseAnonymousUser from flask_security import Security, AnonymousUser as BaseAnonymousUser
@ -35,15 +35,6 @@ class AnonymousUser(BaseAnonymousUser):
return current_app.timezone return current_app.timezone
@property
def profile(self):
"""The profile of the anonymous user
Always evaluates to ``None``
"""
return None
@user_logged_in.connect @user_logged_in.connect
def login_handler(app, user): # pylint: disable=unused-argument def login_handler(app, user): # pylint: disable=unused-argument
@ -54,8 +45,6 @@ def login_handler(app, user): # pylint: disable=unused-argument
AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS)
user.active_sessions += [session.sid]
@user_logged_out.connect @user_logged_out.connect
def logout_handler(app, user): # pylint: disable=unused-argument def logout_handler(app, user): # pylint: disable=unused-argument

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User2_doctor.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid2331" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview>
<g id="g52" transform="matrix(223.2 0 0 228.51 -1.9511e6 -1.9794e6)">
<radialGradient id="XMLID_82_" gradientUnits="userSpaceOnUse" cx="8790" cy="8685.3" r="36.346">
<stop id="stop55" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop57" style="stop-color:#000000" offset="1"/>
</radialGradient>
<circle id="circle59" sodipodi:rx="17.433001" sodipodi:ry="17.433001" style="fill:url(#XMLID_82_)" cx="8782.5" cy="8679.2" sodipodi:cy="8679.21" sodipodi:cx="8782.4932" r="17.433"/>
<linearGradient id="XMLID_83_" y2="8706.5" gradientUnits="userSpaceOnUse" y1="8762" x2="8747.4" x1="8818.9">
<stop id="stop62" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop64" style="stop-color:#000000" offset="1"/>
</linearGradient>
<path id="path66" style="fill:url(#XMLID_83_)" d="m8782.8 8697.6c-15.7 0-28.7 23.1-31 53.3h61.9c-2.2-30.2-15.2-53.3-30.9-53.3z"/>
<path id="path68" style="fill:#c6c7c8" d="m8768.3 8669c-1 1.3-1.8 2.8-2.3 4.4h33c-0.6-1.6-1.3-3.1-2.3-4.4h-28.4z"/>
<circle id="circle70" sodipodi:rx="6.0469999" sodipodi:ry="6.0469999" style="fill:#ffffff" cx="8782.5" cy="8671.2" sodipodi:cy="8671.1943" sodipodi:cx="8782.4932" r="6.047"/>
<circle id="circle72" sodipodi:rx="1.501" sodipodi:ry="1.501" style="fill:#c6c7c8" cx="8782.5" cy="8671.2" sodipodi:cy="8671.1943" sodipodi:cx="8782.4941" r="1.501"/>
<g id="g74">
<circle id="circle76" sodipodi:rx="2.622" sodipodi:ry="2.622" style="stroke:#c6c7c8;stroke-width:.85040;fill:#ffffff" cx="8791" cy="8718.2" sodipodi:cy="8718.166" sodipodi:cx="8790.9648" r="2.622"/>
<g id="g78">
<path id="path80" style="fill:#b3b3b3" d="m8771.9 8714.2c-1.8 0.5-3.2 1.8-4.1 3.5-1 1.8-1.2 4.1-0.5 6.2 0.4 1.5 1.3 2.8 2.5 3.8l0.1 0.1 1.9-0.6-0.5-0.2c-1.2-0.9-2.1-2.1-2.6-3.6-0.5-1.6-0.4-3.4 0.4-4.9 0.7-1.4 1.9-2.4 3.3-2.8 1.4-0.5 2.9-0.3 4.3 0.4 1.5 0.7 2.6 2.1 3.2 3.8 0.4 1.5 0.4 3-0.2 4.4l-0.2 0.5 1.9-0.6v-0.1c0.4-1.5 0.4-3.1-0.1-4.6-1.3-4.2-5.5-6.6-9.4-5.3z"/>
<path id="path82" style="fill:#b2b2b2" d="m8768.5 8723.5c-1.1-3.4 0.6-7.1 3.8-8.1s6.7 1 7.8 4.4c0.5 1.6 0.4 3.2-0.1 4.6l1.2-0.4c0.4-1.4 0.4-2.9-0.1-4.5-1.3-4-5.4-6.3-9.1-5.1-3.8 1.2-5.8 5.4-4.5 9.4 0.5 1.5 1.4 2.8 2.5 3.8l1.2-0.4c-1.2-0.8-2.2-2.1-2.7-3.7z"/>
<path id="path84" style="fill:#b3b3b3" d="m8770.1 8726c-0.8 0.3-1.3 1.2-1 2 0.1 0.4 0.4 0.8 0.8 1 0.4 0.1 0.8 0.2 1.1 0.1 0.4-0.2 0.7-0.4 0.9-0.8 0.2-0.3 0.2-0.8 0.1-1.2-0.2-0.4-0.4-0.8-0.8-1s-0.8-0.2-1.1-0.1z"/>
<ellipse id="ellipse86" sodipodi:rx="1.261" sodipodi:ry="1.3559999" style="fill:#b2b2b2" cx="8770.5" cy="8727.5" rx="1.261" ry="1.356" transform="matrix(.9537 -.3009 .3009 .9537 -2219.4 3043)" sodipodi:cy="8727.5391" sodipodi:cx="8770.5449"/>
<path id="path88" style="fill:#b3b3b3" d="m8780.2 8722.8c-0.4 0.1-0.7 0.4-0.9 0.7-0.1 0.3-0.1 0.5-0.1 0.8v0.5c0.3 0.8 1.2 1.3 2 1.1 0.7-0.3 1.2-1.2 0.9-2s-1.1-1.3-1.9-1.1z"/>
<ellipse id="ellipse90" sodipodi:rx="1.261" sodipodi:ry="1.3559999" style="fill:#b2b2b2" cx="8780.7" cy="8724.3" rx="1.261" ry="1.356" transform="matrix(.9536 -.301 .301 .9536 -2219 3047.9)" sodipodi:cy="8724.3447" sodipodi:cx="8780.6729"/>
</g>
<path id="path92" style="fill:#b3b3b3" d="m8792.1 8715.9l0.8 0.8c2.2-2.6 3.5-6 3.5-9.6 0-1.3-0.1-2.5-0.4-3.7-0.6-0.5-1.1-1-1.7-1.4 0.7 1.6 1 3.3 1 5.1 0 3.3-1.2 6.4-3.2 8.8z"/>
<path id="path94" style="fill:#b3b3b3" d="m8771.4 8715.5l1.1-0.4c-1.6-2.3-2.6-5-2.6-8 0-1.7 0.3-3.3 0.9-4.7-0.6 0.4-1.1 0.9-1.6 1.4-0.3 1-0.4 2.1-0.4 3.3 0 3.1 1 6 2.6 8.4z"/>
</g>
</g>
<g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none">
<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11056/users-by-sampler-11056</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User10_scientist.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview>
<g id="g616" transform="matrix(220.73 0 0 227.54 -1.9119e6 -1.9962e6)">
<linearGradient id="XMLID_112_" y2="8817.8" gradientUnits="userSpaceOnUse" y1="8873.4" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="8667.5" x1="8739.2">
<stop id="stop619" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop621" style="stop-color:#000000" offset="1"/>
</linearGradient>
<path id="path623" style="fill:url(#XMLID_112_)" d="m8703 8808.6c-15.7 0-28.7 23.2-31 53.4l62 0.1c-2.2-30.3-15.2-53.5-31-53.5z"/>
<radialGradient id="XMLID_113_" gradientUnits="userSpaceOnUse" cy="8796.6" cx="8709.6" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="36.411">
<stop id="stop626" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop628" style="stop-color:#000000" offset="1"/>
</radialGradient>
<circle id="circle630" sodipodi:rx="17.464001" sodipodi:ry="17.464001" style="fill:url(#XMLID_113_)" cx="8702.1" cy="8790.2" sodipodi:cy="8790.2412" sodipodi:cx="8702.1367" r="17.464"/>
<g id="g632">
<path id="path634" style="fill:#c6c7c8" d="m8727.6 8800.3c-1.3 0-2.4 1.1-2.4 2.4 0 0.5 0 1.4 0.4 2.2 0.1 0.3 0.3 0.6 0.5 0.8 0.2 0.1 0.3 0.3 0.5 0.4v3.1c-0.5 1-5.8 10.7-5.8 10.7v-0.1c-0.6 1-1 2.7-0.1 4.1 0.8 1.5 2.4 2.2 4.9 2.2h11.3c2.4 0 4.1-0.7 4.9-2.2 0.8-1.4 0.4-3.1-0.2-4.1v0.1s-5.2-9.7-5.8-10.7v-3.1c0.2-0.1 0.4-0.3 0.5-0.4 0.3-0.2 0.4-0.5 0.6-0.8 0.3-0.8 0.3-1.7 0.3-2.2 0-1.3-1-2.4-2.4-2.4h-7.2zm3.5 10.7c0.1-0.1 0.1-0.2 0.1-0.2s0.1 0.1 0.1 0.2c0 0 4.8 8.8 5.6 10.3h-11.3-0.1c0.8-1.5 5.6-10.3 5.6-10.3zm-6.1 11.2v0.1-0.1z"/>
<path id="path636" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:#ffffff" d="m8736.9 8823.7c4.3 0 2.6-2.6 2.6-2.6l-6.1-11.3v-4.7-0.5c0.1-0.6 0.9-0.3 1.3-0.6 0.1-0.5 0.1-1.3 0.1-1.3h-7.2s0 0.8 0.2 1.3c0.3 0.3 1.1 0 1.2 0.6v5.2l-6.1 11.3s-1.6 2.6 2.7 2.6h11.3z"/>
<path id="path638" style="fill:#ffffff" enable-background="new " d="m8733 8810.1c0-0.1-0.1-0.2-0.1-0.3v-5.2c0.1-0.7 0.8-0.8 1.1-0.9 0.1 0 0.2 0 0.2-0.1 0.1-0.1 0.1-0.3 0.1-0.4h-6.2c0 0.1 0 0.3 0.1 0.4 0 0.1 0.2 0.1 0.2 0.1 0.4 0.1 1 0.2 1.1 0.9v5.2c0 0.1 0 0.2-0.1 0.3 0 0-2 3.7-3.7 6.8h11l-3.7-6.8z"/>
<radialGradient id="XMLID_114_" gradientUnits="userSpaceOnUse" cx="8731.2" cy="8820.1" r="6.1747">
<stop id="stop641" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop643" style="stop-color:#C6C7C8" offset="1"/>
</radialGradient>
<path id="path645" style="fill:url(#XMLID_114_)" d="m8736.7 8816.9h-11c-1.3 2.3-2.4 4.4-2.4 4.4s-0.2 0.4-0.2 0.8c0 0.1 0 0.3 0.1 0.4 0.2 0.5 1.1 0.7 2.4 0.7h11.3c1.2 0 2.1-0.2 2.4-0.7 0-0.1 0.1-0.3 0.1-0.4 0-0.4-0.3-0.8-0.3-0.8l-2.4-4.4z"/>
<g id="g647">
<line id="line649" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8729.1" y1="8815.7" x2="8726.1" y2="8815.7"/>
<line id="line651" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8727.4" y1="8818.4" x2="8724.4" y2="8818.4"/>
<line id="line653" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8730.7" y1="8812.6" x2="8727.6" y2="8812.6"/>
<line id="line655" style="stroke-linejoin:round;stroke:#9c9d9f;stroke-width:.95860;stroke-linecap:round;fill:none" x1="8725.9" y1="8821.4" x2="8722.9" y2="8821.4"/>
</g>
<radialGradient id="XMLID_115_" gradientUnits="userSpaceOnUse" cx="8731" cy="8799.4" r="2.1055">
<stop id="stop658" style="stop-color:#F0F3E4" offset=".0112"/>
<stop id="stop660" style="stop-color:#C6C7C8" offset=".4946"/>
<stop id="stop662" style="stop-color:#C6C7C8" offset=".9964"/>
</radialGradient>
<circle id="circle664" sodipodi:rx="2.1059999" sodipodi:ry="2.1059999" style="fill:url(#XMLID_115_)" cx="8731" cy="8799.4" sodipodi:cy="8799.4082" sodipodi:cx="8731.0234" r="2.106"/>
<radialGradient id="XMLID_116_" gradientUnits="userSpaceOnUse" cx="8731.5" cy="8797.4" r="1.2222">
<stop id="stop667" style="stop-color:#F0F3E4" offset=".0112"/>
<stop id="stop669" style="stop-color:#C6C7C8" offset=".4982"/>
<stop id="stop671" style="stop-color:#C9CACB" offset="1"/>
</radialGradient>
<circle id="circle673" sodipodi:rx="1.222" sodipodi:ry="1.222" style="fill:url(#XMLID_116_)" cx="8731.5" cy="8797.4" sodipodi:cy="8797.4023" sodipodi:cx="8731.5215" r="1.222"/>
<radialGradient id="XMLID_117_" gradientUnits="userSpaceOnUse" cx="8730.2" cy="8794.7" r=".65530">
<stop id="stop676" style="stop-color:#F0F3E4" offset=".0112"/>
<stop id="stop678" style="stop-color:#C6C7C8" offset=".4729"/>
<stop id="stop680" style="stop-color:#C6C7C8" offset="1"/>
</radialGradient>
<circle id="circle682" sodipodi:rx="0.65499997" sodipodi:ry="0.65499997" style="fill:url(#XMLID_117_)" cx="8730.2" cy="8794.7" sodipodi:cy="8794.7402" sodipodi:cx="8730.1738" r="0.655"/>
</g>
<linearGradient id="XMLID_118_" y2="8822.4" gradientUnits="userSpaceOnUse" y1="8836.1" x2="8725.3" x1="8742.9">
<stop id="stop685" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop687" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle689" sodipodi:rx="7.96" sodipodi:ry="7.96" style="fill:url(#XMLID_118_)" cx="8734.5" cy="8829.6" sodipodi:cy="8829.5977" sodipodi:cx="8734.5234" r="7.96"/>
<linearGradient id="XMLID_119_" y2="8829.8" gradientUnits="userSpaceOnUse" y1="8832.6" x2="8721.9" x1="8725.5">
<stop id="stop692" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop694" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle696" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_119_)" cx="8723.8" cy="8831.2" sodipodi:cy="8831.2256" sodipodi:cx="8723.7988" r="1.628"/>
<linearGradient id="XMLID_120_" y2="8824" gradientUnits="userSpaceOnUse" y1="8826.8" x2="8722.5" x1="8726.1">
<stop id="stop699" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop701" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle703" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_120_)" cx="8724.4" cy="8825.5" sodipodi:cy="8825.4512" sodipodi:cx="8724.374" r="1.628"/>
<linearGradient id="XMLID_121_" y2="8819.7" gradientUnits="userSpaceOnUse" y1="8822.5" x2="8726.5" x1="8730.1">
<stop id="stop706" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop708" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle710" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_121_)" cx="8728.4" cy="8821.1" sodipodi:cy="8821.1387" sodipodi:cx="8728.3525" r="1.628"/>
<linearGradient id="XMLID_122_" y2="8817.1" gradientUnits="userSpaceOnUse" y1="8819.9" x2="8731.3" x1="8734.9">
<stop id="stop713" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop715" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle717" sodipodi:rx="1.628" sodipodi:ry="1.628" style="fill:url(#XMLID_122_)" cx="8733.2" cy="8818.6" sodipodi:cy="8818.6035" sodipodi:cx="8733.1895" r="1.628"/>
<linearGradient id="XMLID_123_" y2="8817" gradientUnits="userSpaceOnUse" y1="8821.1" x2="8737.2" x1="8742.5">
<stop id="stop720" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop722" style="stop-color:#000000" offset="1"/>
</linearGradient>
<circle id="circle724" sodipodi:rx="2.385" sodipodi:ry="2.385" style="fill:url(#XMLID_123_)" cx="8740" cy="8819.1" sodipodi:cy="8819.1309" sodipodi:cx="8740.0049" r="2.385"/>
<polygon id="polygon726" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" points="8687.8 8828.7 8687.8 8834 8693.3 8836.9 8699.1 8834 8699.1 8828.7"/>
<rect id="rect728" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="2.886" width="11.239" y="8825.8" x="8687.8"/>
<rect id="rect730" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="5.163" width="0.988" y="8820.5" x="8689.4"/>
<rect id="rect732" style="fill:#ffffff" height="0.607" width="1.063" y="8820.5" x="8689.4"/>
<rect id="rect734" style="stroke:#ffffff;stroke-width:.2278;fill:#c6c7c8" height="5.163" width="0.987" y="8820.5" x="8691.3"/>
<rect id="rect736" style="stroke:#ffffff;stroke-width:.2278;fill:#ffffff" height="0.607" width="1.063" y="8821" x="8691.3"/>
<polyline id="polyline738" style="stroke:#ffffff;stroke-width:.2278;stroke-linecap:round;fill:none" points="8692.4 8821.3 8692.8 8821.7 8692.8 8824.2"/>
<line id="line740" style="stroke:#ffffff;stroke-width:.2278;stroke-linecap:round;fill:none" x1="8691.8" y1="8822.9" x2="8691.8" y2="8825.4"/>
</g>
<g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none">
<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11064/users-by-sampler-11064</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg2" sodipodi:docname="sampler_User11_businessman.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="24.827256" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview>
<g id="g1012" transform="matrix(202.56 0 0 211.14 -1.7757e6 -1.8519e6)">
<line id="line1014" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8793.2" x2="8853.8" y2="8793.2"/>
<line id="line1016" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8809.3" x2="8853.8" y2="8809.3"/>
<line id="line1018" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8825.4" x2="8853.8" y2="8825.4"/>
<line id="line1020" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8841.5" x2="8853.8" y2="8841.5"/>
<line id="line1022" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8857.6" x2="8853.8" y2="8857.6"/>
<line id="line1024" style="stroke:#c6c7c8;stroke-width:.2764;fill:none" x1="8768" y1="8777.1" x2="8853.8" y2="8777.1"/>
<linearGradient id="XMLID_129_" y2="8818.6" gradientUnits="userSpaceOnUse" y1="8872.7" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="8771.8" x1="8841.5">
<stop id="stop1027" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop1029" style="stop-color:#000000" offset="1"/>
</linearGradient>
<path id="path1031" style="fill:url(#XMLID_129_)" d="m8806.3 8809.7c-15.3 0-27.9 22.6-30.2 52h60.4c-2.2-29.4-14.9-52-30.2-52z"/>
<radialGradient id="XMLID_130_" gradientUnits="userSpaceOnUse" cy="8797.9" cx="8812.7" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="35.435">
<stop id="stop1034" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop1036" style="stop-color:#000000" offset="1"/>
</radialGradient>
<circle id="circle1038" sodipodi:rx="16.996" sodipodi:ry="16.996" style="fill:url(#XMLID_130_)" cx="8805.4" cy="8791.8" sodipodi:cy="8791.8291" sodipodi:cx="8805.4414" r="16.996"/>
<g id="g1040">
<polyline id="polyline1042" style="stroke-linejoin:round;stroke:#c6c7c8;stroke-width:3.1577;stroke-linecap:round;fill:none" points="8770.2 8837.3 8776.3 8809.9 8786.4 8867.7 8796.5 8830.1 8808.4 8848.4 8822.4 8812.2 8827.5 8833.4 8849.5 8777.1"/>
<circle id="circle1044" sodipodi:rx="4.283" sodipodi:ry="4.283" style="fill:#c6c7c8" cx="8849.5" cy="8777.1" sodipodi:cy="8777.0605" sodipodi:cx="8849.5098" r="4.283"/>
</g>
<g id="g1046">
<path id="path1048" style="fill:#333333" d="m8768.5 8787v-4.4h0.8l1.1 3.1c0.1 0.3 0.1 0.5 0.2 0.6 0-0.1 0.1-0.4 0.2-0.7l1.1-3h0.7v4.4h-0.5v-3.7l-1.3 3.7h-0.5l-1.3-3.7v3.7h-0.5z"/>
<path id="path1050" style="fill:#333333" d="m8773.7 8787l1.7-4.4h0.6l1.8 4.4h-0.7l-0.5-1.4h-1.8l-0.5 1.4h-0.6zm1.2-1.8h1.5l-0.4-1.2c-0.2-0.4-0.3-0.7-0.3-0.9-0.1 0.2-0.2 0.5-0.3 0.8l-0.5 1.3z"/>
<path id="path1052" style="fill:#333333" d="m8778.4 8787l1.7-2.3-1.5-2.1h0.7l0.8 1.1c0.1 0.3 0.3 0.4 0.3 0.6 0.1-0.2 0.2-0.4 0.4-0.5l0.8-1.2h0.7l-1.5 2.1 1.6 2.3h-0.7l-1.1-1.6c-0.1-0.1-0.1-0.2-0.2-0.3-0.1 0.2-0.2 0.3-0.2 0.4l-1.1 1.5h-0.7z"/>
</g>
<g id="g1054">
<path id="path1056" style="fill:#333333" d="m8841.3 8854v-4.4h0.9l1 3.1c0.1 0.3 0.2 0.5 0.2 0.7 0.1-0.2 0.1-0.4 0.3-0.7l1-3.1h0.8v4.4h-0.6v-3.7l-1.3 3.7h-0.5l-1.2-3.7v3.7h-0.6z"/>
<path id="path1058" style="fill:#333333" d="m8847.1 8854v-4.4h0.6v4.4h-0.6z"/>
<path id="path1060" style="fill:#333333" d="m8849.3 8854v-4.4h0.6l2.3 3.4v-3.4h0.5v4.4h-0.6l-2.2-3.4v3.4h-0.6z"/>
</g>
</g>
<g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none">
<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11065/users-by-sampler-11065</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2" sodipodi:docname="sampler_User1_in_suit.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><defs id="defs1434">
<radialGradient id="radialGradient4660" gradientUnits="userSpaceOnUse" cy="8685.3" cx="8710.2" r="36.396" inkscape:collect="always"><stop id="stop35" style="stop-color:#FFFFFF" offset="0"/><stop id="stop37" style="stop-color:#000000" offset="1"/></radialGradient><linearGradient id="linearGradient4662" y2="8706.6" gradientUnits="userSpaceOnUse" x2="8667.6" y1="8762.1" x1="8739.2" inkscape:collect="always"><stop id="stop42" style="stop-color:#FFFFFF" offset="0"/><stop id="stop44" style="stop-color:#000000" offset="1"/></linearGradient></defs><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="5.33" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="g4648" inkscape:cx="50" inkscape:cy="46.515666" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid2323" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview>
<g id="g32" transform="matrix(28.594 0 0 28.594 -2.557e5 -2.4244e5)">
<g id="g4648"><g id="g4654" transform="matrix(7.5835 0 0 8.0832 -56740 -61545)"><circle id="circle39" sodipodi:rx="17.457001" sodipodi:ry="17.457001" style="fill:url(#radialGradient4660)" cx="8702.7" cy="8679.2" sodipodi:cy="8679.2344" sodipodi:cx="8702.7109" r="17.457"/><path id="path46" style="fill:url(#linearGradient4662)" d="m8703 8697.6c-15.7 0-28.7 23.2-31 53.4h62c-2.3-30.2-15.3-53.4-31-53.4z"/><polygon id="polygon48" style="fill:#c6c7c8" points="8700.2 8708 8697.4 8703.1 8703 8698.3 8703 8698.3 8708.6 8703.1 8705.8 8708"/><path id="path50" style="fill:#c6c7c8" d="m8695.4 8737.1l7.6 10.3v-38.7h-2.7l-4.9 28.4zm10.4-28.5h-2.7v38.8l7.6-10.3-4.9-28.5z"/></g></g>
</g>
<g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none">
<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11055/users-by-sampler-11055</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2" sodipodi:docname="sampler_User9_no_idea.svg" xml:space="preserve" overflow="visible" sodipodi:version="0.32" version="1.0" enable-background="new 0 0 18121.891 17875.275" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46dev+devel" viewBox="0 0 18121.891 17875.275"><sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" guidetolerance="10.0" pagecolor="#ffffff" gridtolerance="10.0" width="40px" inkscape:zoom="8.8833333" objecttolerance="10.0" borderlayer="true" borderopacity="1.0" inkscape:current-layer="svg2" inkscape:cx="20" inkscape:cy="30" inkscape:window-y="22" inkscape:window-x="0" inkscape:window-height="744" showgrid="true" inkscape:pageopacity="0.0" inkscape:window-width="1280"><inkscape:grid id="grid3794" originy="0px" originx="0px" spacingy="0px" spacingx="0px" type="xygrid"/></sodipodi:namedview>
<g id="g522" transform="matrix(219.98 0 0 229.49 -2.061e6 -1.9878e6)">
<linearGradient id="XMLID_108_" y2="8705.9" gradientUnits="userSpaceOnUse" y1="8761.1" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" x2="9375.1" x1="9446.2">
<stop id="stop525" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop527" style="stop-color:#000000" offset="1"/>
</linearGradient>
<path id="path529" style="fill:url(#XMLID_108_)" d="m9410.5 8697.4c-15.6-0.1-28.5 22.9-30.8 52.9l61.5 0.1c-2.2-30-15.1-53-30.7-53z"/>
<radialGradient id="XMLID_109_" gradientUnits="userSpaceOnUse" cy="8684.8" cx="9416.8" gradientTransform="matrix(1,8e-4,-8e-4,1,7.1357,-7.2051)" r="36.129">
<stop id="stop532" style="stop-color:#FFFFFF" offset="0"/>
<stop id="stop534" style="stop-color:#000000" offset="1"/>
</radialGradient>
<circle id="circle536" sodipodi:rx="17.329" sodipodi:ry="17.329" style="fill:url(#XMLID_109_)" cx="9409.6" cy="8679.1" sodipodi:cy="8679.1064" sodipodi:cx="9409.5693" r="17.329"/>
<g id="g538">
<g id="g542">
<g id="g544">
<path id="path546" style="stroke:#8e8f91;stroke-width:.56350;fill:#ffffff" d="m9404.8 8668.8c1.2-0.8 2.7-1.2 4.5-1.2 2.3 0 4.3 0.6 5.8 1.7s2.3 2.7 2.3 4.9c0 1.3-0.3 2.5-1 3.4-0.4 0.5-1.1 1.3-2.2 2.1l-1.1 0.9c-0.6 0.4-1 1-1.2 1.6-0.1 0.4-0.2 1-0.2 1.8h-4.2c0-1.7 0.2-2.9 0.5-3.6 0.2-0.7 0.9-1.4 2-2.3l1.2-0.9 0.9-0.9c0.4-0.5 0.6-1.2 0.6-1.8 0-0.8-0.3-1.5-0.7-2.2-0.5-0.6-1.3-0.9-2.5-0.9s-2.1 0.4-2.6 1.1c-0.5 0.8-0.7 1.7-0.7 2.5h-4.5c0.2-2.9 1.2-5 3.1-6.2zm2.6 17.4h4.6v4.4h-4.6v-4.4z"/>
</g>
</g>
</g>
</g>
<g id="OUTLAND" style="display:none" transform="translate(44.606 -3424.8)" display="none">
<path id="path1431" style="display:inline;fill:#9e9994" display="inline" d="m18122 0v17875h-18122v-17875h18122zm-9482 9235.3h841.9v-595.3h-841.9v595.3z"/>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Users</dc:title><dc:date>2007-11-16T10:54:09</dc:date><dc:description/><dc:source>https://openclipart.org/detail/11063/users-by-sampler-11063</dc:source><dc:creator><cc:Agent><dc:title>sampler</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>job</rdf:li><rdf:li>people</rdf:li><rdf:li>profession</rdf:li><rdf:li>user</rdf:li><rdf:li>work</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,121 +0,0 @@
header > h1 > img {
height: 1em;
}
#content {
margin-top: 10px;
}
.ui.profile {
display: block;
position: relative;
height: 50px;
}
.ui.profile > .avatar {
width: 48px;
height: 48px;
border: 1px solid black;
border-radius: 50%;
position: absolute;
}
.ui.profile > .display.name {
position: absolute;
left: 58px;
font-weight: bold;
color: #000000;
}
.ui.profile > .handle {
position: absolute;
top: 1.5em;
left: 58px;
color: #666666;
}
.ui.centered.statistics {
justify-content: center;
}
.timezone-warning {
color: #e94a4a;
}
footer {
margin-top: 3em;
font-weight: bold;
border-top: 1px dotted black;
padding-top: 1em;
}
@media not speech {
.sr-only {
display: none;
}
}
table.calendar > * {
font-family: sans;
}
table.calendar {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
tr.month > td {
text-align: center;
font-weight: bold;
border-bottom: 2px solid black;
padding-bottom: .5em;
}
tr.month > td > a {
font-weight: normal;
color: black;
}
tr.month > td.month-name > a {
font-weight: bold;
}
tr.days > td {
text-align: center;
border-bottom: 2px solid black;
}
tr.week > td {
height: 3em;
}
tr.week > td > span.day-num {
font-weight: bold;
}
tr.week > td.other-month > span.day-num {
font-weight: normal;
color: #909090;
}
tr.week > td.today {
background-color: #d8d8d8;
}
tr.week > td > a.event {
display: block;
color: black;
text-decoration: none;
border: 1px solid green;
background-color: white;
border-radius: 2px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
tr.sizer > td {
width: 14.2857%;
height: 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,30 +0,0 @@
{% macro field(field, inline=false) %}
<div class="{% if inline %}inline {% endif %}field">
{% if field.errors %}
{% for error in field.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{% if field.widget.input_type != 'checkbox' %}
{{ field.label }}
{% endif %}
{{ field }}
{% if field.widget.input_type == 'checkbox' %}
{{ field.label }}<br>
{% endif %}
{% if field.description %}
{{ field.description }}
{% endif %}
</div>
{% endmacro %}
{% macro profile_link(profile) %}
<a href="{% if profile %}{{ url_for('display_profile', username=profile.user.username) }}{% else %}#{% endif %}" class="ui profile">
{% if profile and profile.builtin_avatar %}
<img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular avatar image">
{% endif %}
<div class="display name">{{ profile.display_name }}</div>
<div class="handle">{{ profile or '' }}</div>
</a>
{% endmacro %}

View File

@ -1,15 +0,0 @@
{% extends 'account/settings-base.html' %}
{% block settings_content %}
<h2>{% trans %}Active sessions{% endtrans %}</h2>
<ul>
{% for sess in sessions %}
<li>
{{ sess['ip'] }}
{% if sess.sid != session.sid %}
<a href="{{ url_for('account.invalidate_session', sid=sess.sid) }}">{% trans %}Invalidate{% endtrans %}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock settings_content %}

View File

@ -1,27 +0,0 @@
{% extends 'base.html' %}
{% from '_macros.html' import field %}
{% block content %}
<h1>{% trans %}First steps{% endtrans %}</h1>
<p>
{% trans %}Welcome to Calendar.social!{% endtrans %}
</p>
<p>
{% trans %}These are the first steps you should make before you can start using the site.{% endtrans %}
</p>
<form method="post" class="ui form">
{{ form.hidden_tag() }}
{% if form.errors %}
{% for error in form.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{{ field(form.display_name) }}
{{ field(form.time_zone) }}
<button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button>
</form>
{% endblock content %}

View File

@ -1,23 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>{% trans %}Follow requests{% endtrans %}</h2>
{% if requests.count() %}
<ul>
{% for req in requests %}
<li>
{{ req.follower }}
<a href="{{ url_for('account.accept_follow', follower_id=req.follower_id) }}" class="ui button">{% trans %}Accept{% endtrans %}</a>
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No requests to display.{% endtrans %}
{% endif %}
{% if not current_user.profile.locked %}
<p>
{% trans %}Your profile is not locked.{% endtrans %}
{% trans %}Anyone can follow you without your consent.{% endtrans %}
</p>
{% endif %}
{% endblock content %}

View File

@ -1,22 +0,0 @@
{% extends 'account/settings-base.html' %}
{% from '_macros.html' import field %}
{% block settings_content %}
<h2>{% trans %}Edit profile{% endtrans %}</h2>
<form method="post" class="ui form">
{{ form.hidden_tag() }}
{% if form.errors %}
{% for error in form.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{{ field(form.builtin_avatar) }}
{{ 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

@ -1,19 +0,0 @@
{% extends 'base.html' %}
{% from '_macros.html' import field %}
{% block content %}
<form method="post" class="ui form">
{{ form.hidden_tag() }}
{% for error in form.errors %}
{{ form.errors }}
{% endfor %}
{{ field(form.username) }}
{{ field(form.email) }}
{{ field(form.password) }}
{{ field(form.password_retype) }}
<button type="submit" class="ui primary button">{% trans %}Register{% endtrans %}</button>
</form>
{% endblock %}

View File

@ -1,16 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="ui grid">
<div class="four wide column">
<div class="ui secondary pointing vertical menu">
<a class="item{% if request.endpoint == 'account.edit_profile' %} active{% endif %}" href="{{ url_for('account.edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a>
<a class="item{% if request.endpoint == 'account.settings' %} active{% endif %}" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a>
<a class="item{% if request.endpoint == 'account.active_sessions' %} active{% endif %}" href="{{ url_for('account.active_sessions') }}">{% trans %}Active sessions{% endtrans %}</a>
</div>
</div>
<div class="twelve wide stretched column">
{% block settings_content %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends 'account/settings-base.html' %}
{% from '_macros.html' import field %}
{% block settings_content %}
<h2>{% trans %}Settings{% endtrans %}</h2>
<form method="post" class="ui form">
{{ form.hidden_tag() }}
{% if form.errors %}
{% for error in form.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{{ field(form.timezone) }}
<button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button>
</form>
{% endblock settings_content %}

View File

@ -7,49 +7,47 @@
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}"> <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}"> <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}"> <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}">
{% block head %}
<style>
header > h1 > img {
height: 1em;
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous"> footer {
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}"> margin-top: 3em;
font-weight: bold;
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css"> border-top: 1px dotted black;
padding-top: 1em;
<meta charset="utf-8"> }
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> </style>
{% block head %}{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<header> <header>
<div class="ui top attached menu"> <h1>
<div class="header item"> <img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}"> Calendar.social
Calendar.social </h1>
</div> <nav class="menu">
<div class="right menu"> {% if current_user.is_authenticated %}
{% if not current_user.is_authenticated %} {{ _('Logged in as %(username)s', username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe) }}
<a class="item" href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a>
{% else %}
<div class="item">
{% trans username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe -%}
Logged in as {{username}}
{%- endtrans %}
</div>
<a class="item" href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a>
<a class="item" href="{{ url_for('account.notifications') }}">{% trans %}Notifications{% endtrans %}</a>
<a class="item" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a>
<a class="item" href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a>
{% endif %} {% endif %}
</div> <ul>
</div> {% if not current_user.is_authenticated %}
<li><a href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a></li>
{% else %}
<li><a href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a></li>
<li><a href="{{ url_for('notifications') }}">{% trans %}Notifications{% endtrans %}</a></li>
<li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li>
<li><a href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a></li>
{% endif %}
</ul>
</nav>
</header> </header>
<div class="ui container" id="content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> <footer>
<footer class="ui segment">
<a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br>
Soon…™ Soon…™
</footer> </footer>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='semantic/semantic.min.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -1,29 +1,15 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from '_macros.html' import field %}
{% macro time_zone_warning() %}
{% trans timezone=event.time_zone, start_time=event.start_time_tz | datetimeformat(rebase=false), end_time=event.end_time_tz | datetimeformat(rebase=false) -%}
This event is organised in the {{timezone}} time zone, in which it happens between {{start_time}} and {{end_time}}
{%- endtrans %}
{% endmacro %}
{% block content %} {% block content %}
<h2 class="ui header"> <h1>
<div class="content"> {{ event.title }}<br>
{{ event.title }}<br> <small>
<div class="sub header"> {{ event.start_time_for_user(current_user) }}{{ event.end_time_for_user(current_user) }}
{%- if current_user.timezone | string != event.time_zone -%} {% if current_user.timezone | string != event.time_zone %}
<span title="{{ time_zone_warning() }}"> ({{ event.start_time_tz }}{{ event.end_time_tz }} {{ event.time_zone }})
<i class="fa fa-exclamation-triangle timezone-warning"></i>
<span class="sr-only">{{ time_zone_warning() }}</span>
</span>
{% endif %} {% endif %}
{{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }} </small>
</h1>
{{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }}
</div>
</div>
</h2>
{{ event.description }} {{ event.description }}
<hr> <hr>
<h2>{% trans %}Invited users{% endtrans %}</h2> <h2>{% trans %}Invited users{% endtrans %}</h2>
@ -39,12 +25,13 @@ This event is organised in the {{timezone}} time zone, in which it happens betwe
</ul> </ul>
<hr> <hr>
<h2>{% trans %}Invite{% endtrans %}</h2> <h2>{% trans %}Invite{% endtrans %}</h2>
<form method="post" class="ui form"> <form method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="inline fields">
{{ field(form.invitee, inline=true) }}
<button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button> {{ form.invitee.errors }}
</div> {{ form.invitee.label }}
{{ form.invitee}}
<button type="submit">{% trans %}Invite{% endtrans %}</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,27 +1,43 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from '_macros.html' import field %}
{% block content %} {% block content %}
<h2>Create event</h2> <form method="post">
<form method="post" class="ui form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% if form.errors %} {{ form.errors }}
{% for error in form.errors %}
{{ error }}
{% endfor %}
<br> <br>
{% endif %}
{{ field(form.title) }} {{ form.title.errors }}
{{ field(form.time_zone) }} {{ form.title.label }}
{{ field(form.start_time) }} {{ form.title }}
{{ field(form.end_time) }} <br>
{{ field(form.all_day) }}
{{ field(form.description) }}
{{ field(form.visibility) }}
<button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> {{ form.time_zone.errors }}
<a href="{{ url_for('hello') }}" class="ui button">Cancel</a> {{ form.time_zone.label }}
{{ form.time_zone }}
<br>
{{ form.start_time.errors }}
{{ form.start_time.label }}
{{ form.start_time }}
<br>
{{ form.end_time.errors }}
{{ form.end_time.label }}
{{ form.end_time }}
<br>
{{ form.all_day.errors }}
{{ form.all_day.label }}
{{ form.all_day }}
<br>
{{ form.description.errors }}
{{ form.description.label }}
{{ form.description }}
<br>
<button type="submit">{% trans %}Save{% endtrans %}</button>
<a href="{{ url_for('hello') }}">Cancel</a>
</form> </form>
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block content %}
<h1>{% trans %}First steps{% endtrans %}</h1>
<p>
{% trans %}Welcome to Calendar.social!{% endtrans %}
</p>
<p>
{% trans %}These are the first steps you should make before you can start using the site.{% endtrans %}
</p>
<form method="post">
{{ form.errors }}
{{ form.hidden_tag() }}
<p>
{{ form.display_name.errors }}
{{ form.display_name.label }}
{{ form.display_name }}<br>
{{ form.display_name.description }}
</p>
<p>
{{ form.time_zone.errors }}
{{ form.time_zone.label }}
{{ form.time_zone }}<br>
{{ form.time_zone.description }}
</p>
<button type="submit">{% trans %}Save{% endtrans %}</button>
</form>
{% endblock content %}

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% trans username=current_user.username -%} {{ _('Welcome to Calendar.social, %(username)s!', username=current_user.username) }}
Welcome to Calendar.social, {{username}}!
{%- endtrans %}
{% include 'month-view.html' %} {% include 'month-view.html' %}
<a href="{{ url_for('new_event') }}" class="ui primary button">{% trans %}Add event{% endtrans %}</a> <a href="{{ url_for('new_event') }}">{% trans %}Add event{% endtrans %}</a>
{% endblock content %} {% endblock content %}

View File

@ -1,26 +0,0 @@
{#
FIXME: This template should live under security/ if the app templates would override extension
templates…
#}
{% extends 'base.html' %}
{% from '_macros.html' import field %}
{% block content %}
<h1>{% trans %}Login{% endtrans %}</h1>
<form method="post" class="ui form">
{{ login_user_form.hidden_tag() }}
{% if login_user_form.errors %}
{% for error in login_user_form.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{{ field(login_user_form.email) }}
{{ field(login_user_form.password) }}
{{ field(login_user_form.remember) }}
<button type="submit" class="ui primary button">{% trans %}Login{% endtrans %}</button>
</form>
{% endblock %}

View File

@ -1,3 +1,69 @@
<style>
table.calendar > * {
font-family: sans;
}
table.calendar {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
tr.month > td {
text-align: center;
font-weight: bold;
border-bottom: 2px solid black;
padding-bottom: .5em;
}
tr.month > td > a {
font-weight: normal;
color: black;
}
tr.month > td.month-name > a {
font-weight: bold;
}
tr.days > td {
text-align: center;
border-bottom: 2px solid black;
}
tr.week > td {
height: 3em;
}
tr.week > td > span.day-num {
font-weight: bold;
}
tr.week > td.other-month > span.day-num {
font-weight: normal;
color: #909090;
}
tr.week > td.today {
background-color: #d8d8d8;
}
tr.week > td > a.event {
display: block;
color: black;
text-decoration: none;
border: 1px solid green;
background-color: white;
border-radius: 2px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
tr.sizer > td {
width: 14.2857%;
height: 0;
}
</style>
<table class="calendar"> <table class="calendar">
<thead> <thead>
<tr class="sizer"> <tr class="sizer">
@ -11,10 +77,10 @@
</tr> </tr>
<tr class="month"> <tr class="month">
<td> <td>
<a href="{{ url_for('hello', date=calendar.prev_year.timestamp()) }}">« {{ calendar.prev_year_year }}</a> <a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a>
</td> </td>
<td> <td>
<a href="{{ url_for('hello', date=calendar.prev_month.timestamp()) }}"> {{ calendar.prev_month_name }}</a> <a href="{{ url_for('hello', date=calendar.prev_month) }}"> {{ calendar.prev_month_name }}</a>
</td> </td>
<td colspan="3" class="month-name"> <td colspan="3" class="month-name">
{% if not calendar.has_today %} {% if not calendar.has_today %}
@ -26,10 +92,10 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{{ url_for('hello', date=calendar.next_month.timestamp()) }}">{{ calendar.next_month_name }} </a> <a href="{{ url_for('hello', date=calendar.next_month) }}">{{ calendar.next_month_name }} </a>
</td> </td>
<td> <td>
<a href="{{ url_for('hello', date=calendar.next_year.timestamp()) }}">{{ calendar.next_year_year }} »</a> <a href="{{ url_for('hello', date=calendar.next_year) }}">{{ calendar.next_year_year }} »</a>
</td> </td>
</tr> </tr>
<tr class="days"> <tr class="days">
@ -54,11 +120,9 @@
{%- endif %} {%- endif %}
<td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}"> <td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}">
<span class="day-num">{{ day.day }}</span> <span class="day-num">{{ day.day }}</span>
{% for event in calendar.day_events(day, user=current_user if user_only else none) %} {% for event in calendar.day_events(day, user=current_user) %}
<a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event"> <a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event">
{% if not event.all_day %}
{{ event.start_time_for_user(current_user).strftime('%H:%M') }}{{ event.end_time_for_user(current_user).strftime('%H:%M') }} {{ event.start_time_for_user(current_user).strftime('%H:%M') }}{{ event.end_time_for_user(current_user).strftime('%H:%M') }}
{% endif %}
{{ event.title }} {{ event.title }}
</a> </a>
{% endfor %} {% endfor %}

View File

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

View File

@ -1,18 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from '_macros.html' import profile_link %}
{% block content %} {% block content %}
<h2 class="ui header"> <h1>
{% if profile.builtin_avatar %} {{ profile.name }}
<img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular image">
{% endif %}
{% if profile.locked %}
<i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i>
<span class="sr-only">{% trans %}locked profile{% endtrans %}</span>
{% endif %}
{{ profile.display_name }}
<small>@{{ profile.user.username}}</small> <small>@{{ profile.user.username}}</small>
</h2> </h1>
{% if profile.user != current_user %} {% if profile.user != current_user %}
<a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a>
{% endif %} {% endif %}
@ -22,7 +14,7 @@
</h2> </h2>
{% for followed in profile.followed_list %} {% for followed in profile.followed_list %}
{{ profile_link(followed) }} {{ followed }}
{% endfor %} {% endfor %}
<h2> <h2>
@ -30,6 +22,6 @@
</h2> </h2>
{% for follower in profile.follower_list %} {% for follower in profile.follower_list %}
{{ profile_link(follower) }} {{ follower }}
{% endfor %} {% endfor %}
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% block content %}
<form method="post">
{{ form.errors }}
{{ form.hidden_tag() }}
{{ form.username.errors }}
{{ form.username.label }}
{{ form.username }}
<br>
{{ form.email.errors }}
{{ form.email.label }}
{{ form.email }}
<br>
{{ form.password.errors }}
{{ form.password.label }}
{{ form.password }}
<br>
{{ form.password_retype.errors }}
{{ form.password_retype.label }}
{{ form.password_retype }}
<br>
<button type="submit">{% trans %}Register{% endtrans %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block content %}
<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,83 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from '_macros.html' import profile_link %}
{% block content %} {% block content %}
<div class="ui grid"> <p>Welcome to Calendar.social. There will be lot of content here soon!</p>
<div class="row">
<div class="four wide column">
{% if not current_user.is_authenticated %}
<h2>{% trans %}Login{% endtrans %}</h2>
<form class="ui form" method="post" action="{{ url_for('security.login') }}">
{{ login_form.hidden_tag() }}
{{ login_form.email.label }}
{{ login_form.email }}
{{ login_form.password.label }}
{{ login_form.password }}
<button type="submit" class="fluid ui primary button">{% trans %}Login{% endtrans %}</button>
</form>
{% if config.REGISTRATION_ENABLED %}
<div class="ui horizontal divider">
{% trans %}Or{% endtrans %}
</div>
<a href="{{ url_for('account.register_account' ) }}" class="fluid ui button">{% trans %}Register an account{% endtrans %}</a>
{% endif %}
<div class="ui horizontal divider"></div>
{% endif %}
<h2>{% trans %}What is Calendar.social?{% endtrans %}</h2>
<p>
{% trans %}Calendar.social is a calendar app based on open protocols and free, open source software.{% endtrans %}
{% trans %}It is decentralised like one of its counterparts, email.{% endtrans %}
</p>
{% if current_user.is_authenticated %}
<div class="ui horizontal divider"></div>
<a href="{{ url_for('hello') }}" class="ui fluid primary button">{% trans %}Go to your calendar{% endtrans %}</a>
{% endif %}
</div>
<div class="twelve wide column">
<h2>{% trans %}Peek inside{% endtrans %}</h2>
{% include 'month-view.html' %}
</div>
</div>
<div class="row">
<div class="four wide column">
<h2>{% trans %}Built with users in mind{% endtrans %}</h2>
<p>
{% trans %}From planning your appointments to organising large scale events Calendar.social can help with all your scheduling needs.{% endtrans %}
</p>
</div>
<div class="four wide column">
<div class="ui centered statistics">
<div class="statistic">
<div class="value">{{ user_count }}</div>
<div class="label">
{% trans count=user_count %}user{% pluralize %}users{% endtrans %}
</div>
</div>
<div class="statistic">
<div class="value">{{ event_count }}</div>
<div class="label">
{% trans count=event_count %}event{% pluralize %}events{% endtrans %}
</div>
</div>
</div>
</div>
<div class="four wide column">
<h2>{% trans %}Built for people{% endtrans %}</h2>
<p>
{% trans %}Calendar.social is not a commercial network.{% endtrans %}
{% trans %}No advertising, no data mining, no walled gardens.{% endtrans %}
{% trans %}There is no central authority.{% endtrans %}
</p>
</div>
<div class="four wide column">
{% if admin_profile %}
<h2>{% trans %}Administered by{% endtrans %}</h2>
{{ profile_link(admin_profile) }}
{% endif %}
</div>
</div>
</div>
{% endblock content %} {% endblock content %}

View File

@ -1,15 +1,14 @@
# Hungarian translations for Calendar.social. # Hungarian translations for PROJECT.
# Copyright (C) 2018 Gergely Polonkai # Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the Calendar.social # This file is distributed under the same license as the PROJECT project.
# project. # FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# Gergely Polonkai <gergely@polonkai.eu>, 2018.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.1\n" "Project-Id-Version: 0.1\n"
"Report-Msgid-Bugs-To: gergely@polonkai.eu\n" "Report-Msgid-Bugs-To: gergely@polonkai.eu\n"
"POT-Creation-Date: 2018-07-16 11:09+0200\n" "POT-Creation-Date: 2018-06-29 14:14+0200\n"
"PO-Revision-Date: 2018-07-16 10:37+0200\n" "PO-Revision-Date: 2018-06-29 14:26+0200\n"
"Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n" "Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n"
"Language: hu\n" "Language: hu\n"
"Language-Team: hu <gergely@polonkai.eu>\n" "Language-Team: hu <gergely@polonkai.eu>\n"
@ -19,407 +18,44 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n" "Generated-By: Babel 2.6.0\n"
#: calsocial/forms.py:53 #: app/forms.py:8
msgid "This username is not available"
msgstr "Ez a felhasználónév nem elérhető"
#: calsocial/forms.py:84
msgid "This email address can not be used"
msgstr "Ez az e-mail cím nem használható"
#: calsocial/forms.py:96
msgid "Username" msgid "Username"
msgstr "Felhasználónév" msgstr "Felhasználónév"
#: calsocial/forms.py:97 #: app/forms.py:9
msgid "Email address" msgid "Email address"
msgstr "E-mail cím" msgstr "E-mail cím"
#: calsocial/forms.py:98 #: app/forms.py:10
msgid "Password" msgid "Password"
msgstr "Jelszó" msgstr "Jelszó"
#: calsocial/forms.py:99 #: app/forms.py:11
msgid "Password, once more" msgid "Password, once more"
msgstr "Jelszó még egszer" msgstr "Jelszó még egszer"
#: calsocial/forms.py:108 #: app/forms.py:15
msgid "The two passwords must match!" msgid "The two passwords must match!"
msgstr "A két jelszónak egyeznie kell!" msgstr "A két jelszónak egyeznie kell!"
#: calsocial/forms.py:176 #: app/templates/base.html:21
msgid "Title"
msgstr "Cím"
#: calsocial/forms.py:177 calsocial/forms.py:209
msgid "Time zone"
msgstr "Időzóna"
#: calsocial/forms.py:178
msgid "Start time"
msgstr "Kezdés időpontja"
#: calsocial/forms.py:179
msgid "End time"
msgstr "Befejezés időpontja"
#: calsocial/forms.py:180
msgid "All day"
msgstr "Egész napos"
#: calsocial/forms.py:181
msgid "Description"
msgstr "Leírás"
#: calsocial/forms.py:202
msgid "End time must be later than start time!"
msgstr "A befejezés időpontjának későbbre kell esni, mint a kezdés időpontjának!"
#: calsocial/forms.py:233
msgid "Username or email"
msgstr "Felhasználónév vagy e-mail cím"
#: calsocial/forms.py:321
msgid "User is already invited"
msgstr "Ez a felhasználó már meg van hívva"
#: calsocial/forms.py:331 calsocial/forms.py:345
msgid "Display name"
msgstr "Megjelenítendő név"
#: calsocial/forms.py:334
msgid ""
"This will be shown to other users as your name. You can use your real "
"name, or any nickname you like."
msgstr ""
"Ezt látja majd a többi felhasználó. Megadhatod a valódi neved, vagy a "
"kedvenc beceneved."
#: calsocial/forms.py:336
msgid "Your time zone"
msgstr "Az időzónád"
#: calsocial/forms.py:338
msgid "The start and end times of events will be displayed in this time zone."
msgstr ""
"Az események kezdési és befejezési időpontjai eszerint az időzóna szerint"
" jelennek majd meg."
#: calsocial/forms.py:346
msgid "Lock profile"
msgstr "Profil zárolása"
#: calsocial/models.py:72
#, python-format
msgid "%(actor)s followed you"
msgstr "%(actor)s követ téged"
#: calsocial/models.py:72
#, python-format
msgid "%(actor)s followed %(item)s"
msgstr "%(actor)s követi ezt: %(item)s"
#: calsocial/models.py:73
#, python-format
msgid "%(actor)s invited you to %(item)s"
msgstr "%(actor)s meghívott erre: %(item)s"
#: calsocial/models.py:499
#, python-format
msgid "%(user)s logged in"
msgstr "%(user)s bejelentkezett"
#: calsocial/models.py:500
#, python-format
msgid "%(user)s failed to log in"
msgstr "%(user)s nem tudott bejelentkezni"
#: calsocial/models.py:501
#, python-format
msgid "%(user)s logged out"
msgstr "%(user)s kijelentkezett"
#: calsocial/models.py:518
#, python-format
msgid "UNKNOWN RECORD \"%(log_type)s\" for %(user)s"
msgstr "ISMERETLEN BEJEGYZÉS TÍPUS (\"%(log_type)s\") %(user)s felhasználótól"
#: calsocial/calendar_system/gregorian.py:43
msgid "Gregorian"
msgstr "Gergely naptár"
#: calsocial/calendar_system/gregorian.py:51
msgid "January"
msgstr "Január"
#: calsocial/calendar_system/gregorian.py:52
msgid "February"
msgstr "Február"
#: calsocial/calendar_system/gregorian.py:53
msgid "March"
msgstr "Március"
#: calsocial/calendar_system/gregorian.py:54
msgid "April"
msgstr "Április"
#: calsocial/calendar_system/gregorian.py:55
msgid "May"
msgstr "Május"
#: calsocial/calendar_system/gregorian.py:56
msgid "June"
msgstr "Június"
#: calsocial/calendar_system/gregorian.py:57
msgid "July"
msgstr "Július"
#: calsocial/calendar_system/gregorian.py:58
msgid "August"
msgstr "Augusztus"
#: calsocial/calendar_system/gregorian.py:59
msgid "September"
msgstr "Szeptember"
#: calsocial/calendar_system/gregorian.py:60
msgid "October"
msgstr "Október"
#: calsocial/calendar_system/gregorian.py:61
msgid "November"
msgstr "November"
#: calsocial/calendar_system/gregorian.py:62
msgid "December"
msgstr "December"
#: calsocial/calendar_system/gregorian.py:66
msgid "Monday"
msgstr "Hétfő"
#: calsocial/calendar_system/gregorian.py:67
msgid "Tuesday"
msgstr "Kedd"
#: calsocial/calendar_system/gregorian.py:68
msgid "Wednesday"
msgstr "Szerda"
#: calsocial/calendar_system/gregorian.py:69
msgid "Thursday"
msgstr "Csütörtök"
#: calsocial/calendar_system/gregorian.py:70
msgid "Friday"
msgstr "Péntek"
#: calsocial/calendar_system/gregorian.py:71
msgid "Saturday"
msgstr "Szombat"
#: calsocial/calendar_system/gregorian.py:72
msgid "Sunday"
msgstr "Vasárnap"
#: calsocial/templates/base.html:29 calsocial/templates/login.html:9
#: calsocial/templates/login.html:24 calsocial/templates/welcome.html:7
#: calsocial/templates/welcome.html:14
msgid "Login"
msgstr "Bejelentkezés"
#: calsocial/templates/base.html:32
#, python-format #, python-format
msgid "Logged in as %(username)s" msgid "Logged in as %(username)s"
msgstr "Bejelentkezve %(username)s néven" msgstr "Bejelentkezve %(username)s néven"
#: calsocial/templates/base.html:36 #: app/templates/base.html:25
msgid "Calendar view" msgid "Login"
msgstr "Naptár nézet" msgstr "Bejelentkezés"
#: calsocial/templates/base.html:37 #: app/templates/base.html:27
msgid "Notifications"
msgstr "Értesítések"
#: calsocial/templates/base.html:38 calsocial/templates/settings-base.html:8
#: calsocial/templates/user-settings.html:5
msgid "Settings"
msgstr "Beállítások"
#: calsocial/templates/base.html:39
msgid "Logout" msgid "Logout"
msgstr "Kijelentkezés" msgstr "Kijelentkezés"
#: calsocial/templates/event-details.html:5 #: app/templates/index.html:4
#, python-format
msgid ""
"This event is organised in the %(timezone)s time zone, in which it "
"happens between %(start_time)s and %(end_time)s"
msgstr ""
"Ez az esemény a(z) %(timezone)s időzónában van szervezve, melyben "
"%(start_time)s és %(end_time)s között történik"
#: calsocial/templates/event-details.html:29
msgid "Invited users"
msgstr "Meghívott felhasználók"
#: calsocial/templates/event-details.html:41
#: calsocial/templates/event-details.html:47
msgid "Invite"
msgstr "Meghívás"
#: calsocial/templates/event-edit.html:23
#: calsocial/templates/first-steps.html:25
#: calsocial/templates/profile-edit.html:19
#: calsocial/templates/user-settings.html:18
msgid "Save"
msgstr "Mentés"
#: calsocial/templates/first-steps.html:5
msgid "First steps"
msgstr "Első lépések"
#: calsocial/templates/first-steps.html:7
msgid "Welcome to Calendar.social!"
msgstr "Üdv a Calendar.social-ban!"
#: calsocial/templates/first-steps.html:10
msgid ""
"These are the first steps you should make before you can start using the "
"site."
msgstr ""
"Ezek az első lépések melyeket meg kell tenned, mielőtt elkezded használni"
" az oldalt."
#: calsocial/templates/follow-requests.html:4
msgid "Follow requests"
msgstr "Követési kérések"
#: calsocial/templates/follow-requests.html:10
msgid "Accept"
msgstr "Elfogad"
#: calsocial/templates/follow-requests.html:15
msgid "No requests to display."
msgstr "Nincs megjeleníthető kérés."
#: calsocial/templates/follow-requests.html:19
msgid "Your profile is not locked."
msgstr "A profilod nincs zárolva."
#: calsocial/templates/follow-requests.html:20
msgid "Anyone can follow you without your consent."
msgstr "Bárki követhet a beleegyezésed nélkül."
#: calsocial/templates/index.html:4
#, python-format #, python-format
msgid "Welcome to Calendar.social, %(username)s!" msgid "Welcome to Calendar.social, %(username)s!"
msgstr "Üdv a Calendar.social-ben, %(username)s!" msgstr "Üdv a Calendar.social-ben, %(username)s!"
#: calsocial/templates/index.html:10 #: app/templates/registration.html:27
msgid "Add event"
msgstr "Esemény létrehozása"
#: calsocial/templates/notifications.html:7
msgid "Nothing to show."
msgstr "Nincsenek értesítések."
#: calsocial/templates/profile-details.html:6
#: calsocial/templates/profile-details.html:7
msgid "locked profile"
msgstr "zárolt profil"
#: calsocial/templates/profile-details.html:13
msgid "Follow"
msgstr "Követés"
#: calsocial/templates/profile-details.html:17
msgid "Follows"
msgstr "Követett felhasználók"
#: calsocial/templates/profile-details.html:25
msgid "Followers"
msgstr "Követők"
#: calsocial/templates/profile-edit.html:5
#: calsocial/templates/settings-base.html:7
msgid "Edit profile"
msgstr "Profil szerkesztése"
#: calsocial/templates/registration.html:17
msgid "Register" msgid "Register"
msgstr "Regisztráció" msgstr "Regisztráció"
#: calsocial/templates/welcome.html:18
msgid "Or"
msgstr "Vagy"
#: calsocial/templates/welcome.html:20
msgid "Register an account"
msgstr "Regisztrálj egy fiókot"
#: calsocial/templates/welcome.html:23
msgid "What is Calendar.social?"
msgstr "Mi az a Calendar.social?"
#: calsocial/templates/welcome.html:25
msgid ""
"Calendar.social is a calendar app based on open protocols and free, open "
"source software."
msgstr ""
"A Calendar.social egy naptár alkalmazás, mely nyílt protokollokat és "
"szabad, nyílt forráskódú szoftvereket használ."
#: calsocial/templates/welcome.html:26
msgid "It is decentralised like one of its counterparts, email."
msgstr "Decentralizált, mint legismertebb társa, az e-mail."
#: calsocial/templates/welcome.html:30
msgid "Peek inside"
msgstr "Less be"
#: calsocial/templates/welcome.html:36
msgid "Built for users in mind"
msgstr "A felhasználók igényeire szabva"
#: calsocial/templates/welcome.html:38
#, fuzzy
msgid ""
"From planning your appointments to organising large scale events "
"Calendar.social can help with all your scheduling needs."
msgstr ""
"Találkozók tervezésétől a nagyméretű rendezvények szervezéséig, a "
"Calendar.social segít az ütemezésben."
#: calsocial/templates/welcome.html:47
msgid "user"
msgid_plural "users"
msgstr[0] "felhasználó"
#: calsocial/templates/welcome.html:53
msgid "event"
msgid_plural "events"
msgstr[0] "esemény"
#: calsocial/templates/welcome.html:60
msgid "Built for people"
msgstr "Embereknek készítve"
#: calsocial/templates/welcome.html:62
msgid "Calendar.social is not a commercial network."
msgstr "A Calendar.social nem egy kereskedelmi hálózat."
#: calsocial/templates/welcome.html:63
msgid "No advertising, no data mining, no walled gardens."
msgstr "Nincs reklám, nincs adatbányászat, nincsenek korlátok."
#: calsocial/templates/welcome.html:64
msgid "There is no central authority."
msgstr "Nincs központi autoritás."
#: calsocial/templates/welcome.html:69
msgid "Administered by"
msgstr "Üzemelteti"

View File

@ -1,462 +0,0 @@
# Translations template for Calendar.social.
# Copyright (C) 2018 Sylke Vicious
# This file is distributed under the same license as the Calendar.social project.
# Sylke Vicious <silkevicious@layer8.space>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.1\n"
"Report-Msgid-Bugs-To: gergely@polonkai.eu\n"
"POT-Creation-Date: 2018-07-31 09:55+0200\n"
"PO-Revision-Date: 2018-07-31 09:56+0200\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
"X-Generator: Poedit 2.0.9\n"
"Last-Translator: Sylke Vicious <silkevicious@layer8.space>\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"Language: it\n"
#: calsocial/account.py:227
msgid "Cant invalidate your current session"
msgstr "Non puoi revocare la tua sessione corrente"
#: calsocial/forms.py:57
msgid "This username is not available"
msgstr "Questo nome utente non è disponibile"
#: calsocial/forms.py:88
msgid "This email address can not be used"
msgstr "Questo indirizzo email non può essere usato"
#: calsocial/forms.py:100
msgid "Username"
msgstr "Nome utente"
#: calsocial/forms.py:101
msgid "Email address"
msgstr "Indirizzo email"
#: calsocial/forms.py:102
msgid "Password"
msgstr "Password"
#: calsocial/forms.py:103
msgid "Password, once more"
msgstr "Ripeti la password"
#: calsocial/forms.py:112
msgid "The two passwords must match!"
msgstr "Le due password devono corrispondere!"
#: calsocial/forms.py:219
msgid "Title"
msgstr "Titolo"
#: calsocial/forms.py:220 calsocial/forms.py:260
msgid "Time zone"
msgstr "Fuso orario"
#: calsocial/forms.py:221
msgid "Start time"
msgstr "Ora d'inizio"
#: calsocial/forms.py:222
msgid "End time"
msgstr "Ora di fine"
#: calsocial/forms.py:223
msgid "All day"
msgstr "Tutto il giorno"
#: calsocial/forms.py:224
msgid "Description"
msgstr "Descrizione"
#: calsocial/forms.py:225
msgid "Visibility"
msgstr "Visibilità"
#: calsocial/forms.py:253
msgid "End time must be later than start time!"
msgstr ""
#: calsocial/forms.py:284
msgid "Username or email"
msgstr "Nome utente o email"
#: calsocial/forms.py:372
msgid "User is already invited"
msgstr "L'utente è già invitato"
#: calsocial/forms.py:382 calsocial/forms.py:396
msgid "Display name"
msgstr "Mostra nome"
#: calsocial/forms.py:385
msgid ""
"This will be shown to other users as your name. You can use your real "
"name, or any nickname you like."
msgstr "Questo sarà mostrato agli altri utenti come tuo nome. Puoi usare il tuo vero "
"nome, o qualsiasi altro soprannome che preferisci."
#: calsocial/forms.py:387
msgid "Your time zone"
msgstr "Il tuo fuso orario"
#: calsocial/forms.py:389
msgid "The start and end times of events will be displayed in this time zone."
msgstr "L'ora di inizio e di fine degli eventi sarà mostrata con questo fuso orario"
#: calsocial/forms.py:397
msgid "Use a built-in avatar"
msgstr "Usa un avatar predefinito"
#: calsocial/forms.py:398
msgid "Lock profile"
msgstr "Blocca profilo"
#: calsocial/models.py:75
#, python-format
msgid "%(actor)s followed you"
msgstr "%(actor)s ti segue"
#: calsocial/models.py:75
#, python-format
msgid "%(actor)s followed %(item)s"
msgstr "%(actor)s segue %(item)s"
#: calsocial/models.py:76
#, python-format
msgid "%(actor)s invited you to %(item)s"
msgstr "%(actor)s ti ha invitato a %(item)s"
#: calsocial/models.py:121
msgid "Visible only to attendees"
msgstr "Visibile solo ai partecipanti"
#: calsocial/models.py:122
msgid "Visible to everyone"
msgstr "Visibile a tutti"
#: calsocial/models.py:542
#, python-format
msgid "%(user)s logged in"
msgstr "%(user)s ha effettuato l'accesso"
#: calsocial/models.py:543
#, python-format
msgid "%(user)s failed to log in"
msgstr "%(user)s non è riuscito ad accedere"
#: calsocial/models.py:544
#, python-format
msgid "%(user)s logged out"
msgstr "%(user)s si è disconnesso"
#: calsocial/models.py:561
#, python-format
msgid "UNKNOWN RECORD \"%(log_type)s\" for %(user)s"
msgstr "RECORD SCONOSCIUTO \"%(log_type)s\" per %(user)s"
#: calsocial/calendar_system/gregorian.py:31
msgid "Gregorian"
msgstr "Gregoriano"
#: calsocial/calendar_system/gregorian.py:39
msgid "January"
msgstr "Gennaio"
#: calsocial/calendar_system/gregorian.py:40
msgid "February"
msgstr "Febbraio"
#: calsocial/calendar_system/gregorian.py:41
msgid "March"
msgstr "Marzo"
#: calsocial/calendar_system/gregorian.py:42
msgid "April"
msgstr "Aprile"
#: calsocial/calendar_system/gregorian.py:43
msgid "May"
msgstr "Maggio"
#: calsocial/calendar_system/gregorian.py:44
msgid "June"
msgstr "Giugno"
#: calsocial/calendar_system/gregorian.py:45
msgid "July"
msgstr "Luglio"
#: calsocial/calendar_system/gregorian.py:46
msgid "August"
msgstr "Agosto"
#: calsocial/calendar_system/gregorian.py:47
msgid "September"
msgstr "Settembre"
#: calsocial/calendar_system/gregorian.py:48
msgid "October"
msgstr "Ottobre"
#: calsocial/calendar_system/gregorian.py:49
msgid "November"
msgstr "Novembre"
#: calsocial/calendar_system/gregorian.py:50
msgid "December"
msgstr "Dicembre"
#: calsocial/calendar_system/gregorian.py:54
msgid "Monday"
msgstr "Lunedì"
#: calsocial/calendar_system/gregorian.py:55
msgid "Tuesday"
msgstr "Martedì"
#: calsocial/calendar_system/gregorian.py:56
msgid "Wednesday"
msgstr "Mercoledì"
#: calsocial/calendar_system/gregorian.py:57
msgid "Thursday"
msgstr "Giovedì"
#: calsocial/calendar_system/gregorian.py:58
msgid "Friday"
msgstr "Venerdì"
#: calsocial/calendar_system/gregorian.py:59
msgid "Saturday"
msgstr "Sabato"
#: calsocial/calendar_system/gregorian.py:60
msgid "Sunday"
msgstr "Domenica"
#: calsocial/templates/base.html:29 calsocial/templates/login.html:9
#: calsocial/templates/login.html:24 calsocial/templates/welcome.html:9
#: calsocial/templates/welcome.html:16
msgid "Login"
msgstr "Accedi"
#: calsocial/templates/base.html:32
#, python-format
msgid "Logged in as %(username)s"
msgstr "Effettuato l'accesso come %(username)s"
#: calsocial/templates/base.html:36
msgid "Calendar view"
msgstr "Vista calendario"
#: calsocial/templates/base.html:37
msgid "Notifications"
msgstr "Notifiche"
#: calsocial/templates/account/settings-base.html:8
#: calsocial/templates/account/user-settings.html:5
#: calsocial/templates/base.html:38
msgid "Settings"
msgstr "Impostazioni"
#: calsocial/templates/base.html:39
msgid "Logout"
msgstr "Disconnettiti"
#: calsocial/templates/base.html:48
msgid "About this instance"
msgstr "A proposito di questa istanza"
#: 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 ""
"Questo evento è organizzato nel fuso orario %(timezone)s, nel quale "
"avverrà tra le %(start_time)s e le %(end_time)s"
#: calsocial/templates/event-details.html:29
msgid "Invited users"
msgstr "Utenti invitati"
#: calsocial/templates/event-details.html:41
#: calsocial/templates/event-details.html:47
msgid "Invite"
msgstr "Invita"
#: calsocial/templates/account/first-steps.html:25
#: calsocial/templates/account/profile-edit.html:20
#: calsocial/templates/account/user-settings.html:18
#: calsocial/templates/event-edit.html:24
msgid "Save"
msgstr "Salva"
#: calsocial/templates/index.html:4
#, python-format
msgid "Welcome to Calendar.social, %(username)s!"
msgstr "Benvenuto su Calendar.social, %(username)s!"
#: calsocial/templates/index.html:10
msgid "Add event"
msgstr "Crea evento"
#: calsocial/templates/profile-details.html:10
#: calsocial/templates/profile-details.html:11
msgid "locked profile"
msgstr "profilo bloccato"
#: calsocial/templates/profile-details.html:17
msgid "Follow"
msgstr "Segui"
#: calsocial/templates/profile-details.html:21
msgid "Follows"
msgstr "Seguendo"
#: calsocial/templates/profile-details.html:29
msgid "Followers"
msgstr "Seguito da"
#: calsocial/templates/welcome.html:20
msgid "Or"
msgstr "O"
#: calsocial/templates/welcome.html:22
msgid "Register an account"
msgstr "Registra un account"
#: calsocial/templates/welcome.html:26
msgid "What is Calendar.social?"
msgstr "Cos'è Calendar.social"
#: calsocial/templates/welcome.html:28
msgid ""
"Calendar.social is a calendar app based on open protocols and free, open "
"source software."
msgstr ""
"Calendar.social è un'app calendario basata su protocolli aperti e software "
"open source."
#: calsocial/templates/welcome.html:29
msgid "It is decentralised like one of its counterparts, email."
msgstr "È decentralizzato come una delle sue controparti, l'email"
#: calsocial/templates/welcome.html:33
msgid "Go to your calendar"
msgstr "Vai al tuo calendario"
#: calsocial/templates/welcome.html:37
msgid "Peek inside"
msgstr "Uno sguardo all'interno"
#: calsocial/templates/welcome.html:43
msgid "Built with users in mind"
msgstr "Creato pensando agli utenti"
#: calsocial/templates/welcome.html:45
msgid ""
"From planning your appointments to organising large scale events "
"Calendar.social can help with all your scheduling needs."
msgstr ""
"Dalla pianificazione dei tuoi appuntamenti all'organizzazione di eventi su larga scala "
"Calendar.social ti può aiutare con tutte le tue esigenze di organizzazione."
#: calsocial/templates/welcome.html:54
msgid "user"
msgid_plural "users"
msgstr[0] "utente"
msgstr[1] "utenti"
#: calsocial/templates/welcome.html:60
msgid "event"
msgid_plural "events"
msgstr[0] "evento"
msgstr[1] "eventi"
#: calsocial/templates/welcome.html:67
msgid "Built for people"
msgstr "Creato per le persone"
#: calsocial/templates/welcome.html:69
msgid "Calendar.social is not a commercial network."
msgstr "Calendar.social non è una rete commerciale."
#: calsocial/templates/welcome.html:70
msgid "No advertising, no data mining, no walled gardens."
msgstr "No pubblicità, no analisi dei dati, no restrizioni proprietarie"
#: calsocial/templates/welcome.html:71
msgid "There is no central authority."
msgstr "Non esiste un'amministrazione centrale"
#: calsocial/templates/welcome.html:77
msgid "Administered by"
msgstr "Amministrato da"
#: calsocial/templates/account/active-sessions.html:4
#: calsocial/templates/account/settings-base.html:9
msgid "Active sessions"
msgstr "Sessioni attive"
#: calsocial/templates/account/active-sessions.html:10
msgid "Invalidate"
msgstr "Revoca"
#: calsocial/templates/account/first-steps.html:5
msgid "First steps"
msgstr "Primi passi"
#: calsocial/templates/account/first-steps.html:7
msgid "Welcome to Calendar.social!"
msgstr "Benvenuto su Calendar.social!"
#: calsocial/templates/account/first-steps.html:10
msgid ""
"These are the first steps you should make before you can start using the "
"site."
msgstr ""
"Questi sono i primi passi che dovresti fare prima di iniziare ad usare "
"il sito."
#: calsocial/templates/account/follow-requests.html:4
msgid "Follow requests"
msgstr "Richieste di seguirti"
#: calsocial/templates/account/follow-requests.html:10
msgid "Accept"
msgstr "Accetta"
#: calsocial/templates/account/follow-requests.html:15
msgid "No requests to display."
msgstr "Nessuna richiesta da mostrare."
#: calsocial/templates/account/follow-requests.html:19
msgid "Your profile is not locked."
msgstr "Il tuo profilo non è bloccato."
#: calsocial/templates/account/follow-requests.html:20
msgid "Anyone can follow you without your consent."
msgstr "Chiunque può seguirti senza chiederti il permesso."
#: calsocial/templates/account/notifications.html:7
msgid "Nothing to show."
msgstr "Niente da mostrare."
#: calsocial/templates/account/profile-edit.html:5
#: calsocial/templates/account/settings-base.html:7
msgid "Edit profile"
msgstr "Modifica profilo"
#: calsocial/templates/account/registration.html:17
msgid "Register"
msgstr "Registrati"

View File

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

View File

@ -68,54 +68,3 @@ def force_locale(locale):
babel.locale_selector_func = orig_locale_selector_func babel.locale_selector_func = orig_locale_selector_func
for key, value in orig_attrs.items(): for key, value in orig_attrs.items():
setattr(ctx, key, value) setattr(ctx, key, value)
class RoutedMixin:
"""Mixin to lazily register class methods as routes
Works both for `Flask` and `Blueprint` objects.
Example::
class MyBlueprint(Blueprint, RoutedMixin):
def __init__(self, *args, **kwargs):
do_whatever_you_like()
RoutedMixin.register_routes(self)
@RoutedMixin.route('/')
def index(self):
return 'Hello, World!'
"""
def register_routes(self):
"""Register all routes that were marked with :meth:`route`
"""
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

View File

@ -1,62 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Helper functions and fixtures for testing
"""
from contextlib import contextmanager
import pytest
from helpers import configure_app
@pytest.fixture
def client():
"""Fixture that provides a Flask test client
"""
from calsocial import app
from calsocial.models import db
configure_app()
client = app.test_client()
with app.app_context():
db.create_all()
yield client
with app.app_context():
db.drop_all()
@pytest.fixture
def database():
"""Fixture to provide all database tables in an active application context
"""
from calsocial import app
from calsocial.models import db
configure_app()
with app.app_context():
db.create_all()
yield db
db.drop_all()

View File

@ -1,60 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Helper functions for testing
"""
from contextlib import contextmanager
import calsocial
from calsocial.models import db
def configure_app():
"""Set default configuration values for testing
"""
calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
calsocial.app.config['TESTING'] = True
calsocial.app.config['WTF_CSRF_ENABLED'] = False
def login(client, username, password, no_redirect=False):
"""Login with the specified username and password
"""
return client.post('/login',
data={'email': username, 'password': password},
follow_redirects=not no_redirect)
@contextmanager
def alter_config(app, **kwargs):
saved = {}
for key, value in kwargs.items():
if key in app.config:
saved[key] = app.config[key]
app.config[key] = value
yield
for key, value in kwargs.items():
if key in saved:
app.config[key] = saved[key]
else:
del app.config[key]

View File

@ -1,49 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
def test_app_state_set(database):
from calsocial.models import db, AppState
AppState['test'] = 'value'
state = AppState.query \
.filter(AppState.env == 'testing') \
.filter(AppState.key == 'test') \
.one()
assert state.value == 'value'
def test_app_state_get(database):
from calsocial.models import db, AppState
state = AppState(env='testing', key='test', value='value')
db.session.add(state)
db.session.commit()
assert AppState['test'] == 'value'
def test_app_state_setdefault(database):
from calsocial.models import AppState
AppState['test'] = 'value'
AppState.setdefault('test', 'new value')
assert AppState['test'] == 'value'
AppState.setdefault('other_test', 'value')
assert AppState['other_test'] == 'value'

View File

@ -1,76 +1,121 @@
# 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/>.
"""General tests for Calendar.social
"""
from flask import current_app
import pytest import pytest
import calsocial
from calsocial.models import db, User
@pytest.fixture
def client():
calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
calsocial.app.config['TESTING'] = True
calsocial.app.config['WTF_CSRF_ENABLED'] = False
client = calsocial.app.test_client()
with calsocial.app.app_context():
db.create_all()
yield client
with calsocial.app.app_context():
db.drop_all()
def test_index_no_login(client): def test_index_no_login(client):
"""Test the main page without logging in """Test the main page without logging in
""" """
page = client.get('/') page = client.get('/')
assert b'Peek inside' in page.data assert b'Welcome to Calendar.social' in page.data
def test_instance_adin_unset(database): def test_register_page(client):
"""Test the instance admin feature if the admin is not set """Test the registration page
""" """
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'): page = client.get('/register')
assert current_app.instance_admin is None assert b'Register</button>' in page.data
def test_register_post_empty(client):
def test_instance_admin_bad_value(database): """Test sending empty registration data
"""Test the instance admin feature if the value is invalid
""" """
from calsocial.models import AppState page = client.post('/register', data={})
assert b'This field is required' in page.data
AppState['instance_admin'] = 'value' def test_register_invalid_email(client):
"""Test sending an invalid email address
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'):
assert current_app.instance_admin is None
def test_instance_admin_doesnot_exist(database):
"""Test the instance admin feature if the admin user does not exist
""" """
from calsocial.models import AppState page = client.post('/register', data={
'username': 'test',
'email': 'test',
'password': 'password',
'password_retype': 'password',
})
assert b'Invalid email address' in page.data
AppState['instance_admin'] = '0' def test_register_password_mismatch(client):
"""Test sending different password for registration
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'):
assert current_app.instance_admin is None
def test_instance_admin(database):
"""Test the instance admin feature if the admin user does not exist
""" """
from calsocial.models import db, AppState, User page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'something',
})
assert b'The two passwords must match' in page.data
user = User(username='admin') def test_register(client):
db.session.add(user) """Test user registration
db.session.commit() """
AppState['instance_admin'] = user.id page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
print(page.data)
assert page.status_code == 302
assert page.location == 'http://localhost/'
assert current_app.instance_admin == user with calsocial.app.app_context():
user = User.query.one()
assert user.username == 'test'
assert user.email == 'test@example.com'
def test_register_existing_username(client):
"""Test registering an existing username
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This username is not available' in page.data
def test_register_existing_email(client):
"""Test registering an existing email address
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'tester',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This email address can not be used' in page.data

View File

@ -1,117 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Profile related tests for Calendar.social
"""
import calsocial
from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow
from helpers import login
def test_profile_follow(database):
"""Test the Profile.follow() method
"""
follower_user = User(username='follower',
email='follower@example.com',
password='passworder',
active=True)
followed_user = User(username='followed', email='followed@example.com')
follower = Profile(display_name='Follower', user=follower_user)
followed = Profile(display_name='Followed', user=followed_user)
db.session.add_all([follower, followed])
db.session.commit()
user_follow = followed.follow(follower=follower)
db.session.commit()
# The new follower record should have the fields set correctly
assert user_follow.followed == followed
assert user_follow.follower == follower
# There should be a notification about the follow
notification = Notification.query.one()
assert notification.actor == follower
assert notification.item == followed
assert notification.action == NotificationAction.follow
assert follower in followed.follower_list
assert followed in follower.followed_list
def test_follow_ui(client):
"""Test following on the web interface
"""
with calsocial.app.app_context():
follower_user = User(username='follower',
email='follower@example.com',
password='passworder',
active=True)
followed_user = User(username='followed', email='followed@example.com')
follower = Profile(display_name='Follower', user=follower_user)
followed = Profile(display_name='Followed', user=followed_user)
db.session.add_all([follower, followed])
db.session.commit()
login(client, 'follower', 'passworder')
page = client.get('/profile/@followed/follow')
assert page.status_code == 302
assert page.location == 'http://localhost/profile/%40followed'
with calsocial.app.app_context():
db.session.add_all([follower, followed])
follow = UserFollow.query.one()
assert follow.follower == follower
assert follow.followed == followed
assert follow.accepted_at is not None
def test_locked_profile(database):
"""Test following a locked profile
"""
follower_user = User(username='follower',
email='follower@example.com',
password='passworder',
active=True)
followed_user = User(username='followed', email='followed@example.com')
follower = Profile(display_name='Follower', user=follower_user)
followed = Profile(display_name='Followed', user=followed_user, locked=True)
db.session.add_all([follower, followed])
db.session.commit()
user_follow = followed.follow(follower=follower)
db.session.commit()
# The new follower record should have the fields set correctly
assert user_follow.followed == followed
assert user_follow.follower == follower
assert not user_follow.accepted_at
# There should be a notification about the follow
notification = Notification.query.one()
assert notification.actor == follower
assert notification.item == followed
assert notification.action == NotificationAction.follow
assert follower not in followed.follower_list
assert followed not in follower.followed_list

View File

@ -1,92 +0,0 @@
from datetime import datetime, date
from pytz import utc
from calsocial.calendar_system.gregorian import GregorianCalendar
def test_day_list():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.days[0].date() == date(2018, 1, 1)
assert calendar.days[-1].date() == date(2018, 2, 4)
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.days[0].date() == date(2018, 11, 26)
assert calendar.days[-1].date() == date(2019, 1, 6)
def test_prev_year():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.prev_year == datetime(2017, 1, 1, 0, 0, 0)
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.prev_year == datetime(2017, 12, 1, 0, 0, 0)
def test_prev_year_year():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.prev_year_year == 2017
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.prev_year_year == 2017
def test_prev_month():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.prev_month == datetime(2017, 12, 1, 0, 0, 0)
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.prev_month == datetime(2018, 11, 1, 0, 0, 0)
def test_prev_month_name():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.prev_month_name == 'December'
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.prev_month_name == 'November'
def test_next_year():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.next_year == datetime(2019, 1, 1, 0, 0, 0)
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.next_year == datetime(2019, 12, 1, 0, 0, 0)
def test_next_year_year():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.next_year_year == 2019
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.next_year_year == 2019
def test_next_month():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.next_month == datetime(2018, 2, 1, 0, 0, 0)
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.next_month == datetime(2019, 1, 1, 0, 0, 0)
def test_next_month_name():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.next_month_name == 'February'
calendar = GregorianCalendar(utc.localize(datetime(2018, 12, 1, 0, 0, 0)).timestamp())
assert calendar.next_month_name == 'January'
def test_has_today():
calendar = GregorianCalendar(utc.localize(datetime(1990, 12, 1, 0, 0, 0)).timestamp())
assert calendar.has_today is False
calendar = GregorianCalendar(utc.localize(datetime.utcnow()).timestamp())
assert calendar.has_today is True
def test_current_month():
calendar = GregorianCalendar(utc.localize(datetime(2018, 1, 1, 0, 0, 0)).timestamp())
assert calendar.month == 'January, 2018'

View File

@ -1,76 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""General tests for Calendar.social
"""
import calsocial
from calsocial.models import db, User
from helpers import login
def test_login_invalid_user(client):
"""Test logging in with a non-existing user
"""
page = login(client, 'username', 'password')
assert b'Specified user does not exist' in page.data
def test_login_bad_password(client):
"""Test logging in with a bad password
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com', password='password')
db.session.add(user)
db.session.commit()
page = login(client, 'test', password='else')
assert b'Invalid password' in page.data
def test_login_email(client):
"""Test logging in with the email address instead of the username
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com', password='password', active=True)
db.session.add(user)
db.session.commit()
page = login(client, 'test@example.com', password='password')
assert b'Logged in as ' in page.data
def test_login_first_steps(client):
"""Test logging in with a new user
They must be redirected to the first login page
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com', password='password', active=True)
db.session.add(user)
db.session.commit()
page = login(client, 'test', password='password', no_redirect=True)
# First, we must be redirected to the main page
assert page.location == 'http://localhost/'
page = client.get('/')
assert page.location == 'http://localhost/accounts/first-steps'

View File

@ -1,121 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""General tests for Calendar.social
"""
import calsocial
from calsocial.models import db, User
from helpers import alter_config
def test_register_page(client):
"""Test the registration page
"""
page = client.get('/accounts/register')
assert b'Register</button>' in page.data
def test_register_post_empty(client):
"""Test sending empty registration data
"""
page = client.post('/accounts/register', data={})
assert b'This field is required' in page.data
def test_register_invalid_email(client):
"""Test sending an invalid email address
"""
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test',
'password': 'password',
'password_retype': 'password',
})
assert b'Invalid email address' in page.data
def test_register_password_mismatch(client):
"""Test sending different password for registration
"""
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'something',
})
assert b'The two passwords must match' in page.data
def test_register(client):
"""Test user registration
"""
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert page.status_code == 302
assert page.location == 'http://localhost/'
with calsocial.app.app_context():
user = User.query.one()
assert user.username == 'test'
assert user.email == 'test@example.com'
def test_register_existing_username(client):
"""Test registering an existing username
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This username is not available' in page.data
def test_register_existing_email(client):
"""Test registering an existing email address
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
'username': 'tester',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This email address can not be used' in page.data
def test_registration_disabled(client):
with alter_config(calsocial.app, REGISTRATION_ENABLED=False):
page = client.get('/accounts/register')
assert b'Registration is disabled' in page.data