Compare commits

..

1 Commits

Author SHA1 Message Date
06c7b2ea52 Add the Response model
This is to store RSVPs.
2018-07-12 12:22:49 +02:00
57 changed files with 618 additions and 3204 deletions

View File

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

3
.gitignore vendored
View File

@ -3,6 +3,3 @@ __pycache__/
/messages.pot
/calsocial/translations/*/LC_MESSAGES/*.mo
/.pytest_cache/
/.env
/.coverage
/htmlcov/

View File

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

107
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294"
"sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7"
},
"pipfile-spec": 6,
"requires": {
@ -128,14 +128,6 @@
"index": "pypi",
"version": "==0.9.3"
},
"flask-caching": {
"hashes": [
"sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227",
"sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985"
],
"index": "pypi",
"version": "==1.4.0"
},
"flask-login": {
"hashes": [
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
@ -255,9 +247,9 @@
},
"sqlalchemy": {
"hashes": [
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
"sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957"
],
"version": "==1.2.10"
"version": "==1.2.9"
},
"sqlalchemy-utils": {
"hashes": [
@ -284,10 +276,10 @@
"develop": {
"astroid": {
"hashes": [
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f"
"sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a",
"sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a"
],
"version": "==2.0.1"
"version": "==1.6.5"
},
"atomicwrites": {
"hashes": [
@ -303,50 +295,6 @@
],
"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": {
"hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
@ -421,11 +369,11 @@
},
"pylint": {
"hashes": [
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251"
"sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c",
"sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3"
],
"index": "pypi",
"version": "==2.0.1"
"version": "==1.9.2"
},
"pytest": {
"hashes": [
@ -435,14 +383,6 @@
"index": "pypi",
"version": "==3.6.3"
},
"pytest-cov": {
"hashes": [
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
],
"index": "pypi",
"version": "==2.5.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
@ -450,35 +390,6 @@
],
"version": "==1.11.0"
},
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"wrapt": {
"hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"

View File

@ -19,18 +19,12 @@
from datetime import datetime
import os
from warnings import warn
from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \
url_for
from flask import Flask, abort, current_app, redirect, render_template, request, url_for
from flask_babelex import Babel, get_locale as babel_get_locale
from flask_security import SQLAlchemyUserDatastore, current_user, login_required
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from calsocial.account import AccountBlueprint
from calsocial.cache import CachedSessionInterface, cache
from calsocial.utils import RoutedMixin
def get_locale():
"""Locale selector
@ -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
"""
@ -70,32 +79,13 @@ class CalendarSocialApp(Flask, RoutedMixin):
Flask.__init__(self, name)
self.session_interface = CachedSessionInterface()
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)
# Make sure we look up users both by their usernames and email addresses
self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email')
self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html'
# 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)
cache.init_app(self)
babel = Babel(app=self)
babel.localeselector(get_locale)
@ -106,9 +96,18 @@ class CalendarSocialApp(Flask, RoutedMixin):
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)
@ -119,8 +118,8 @@ class CalendarSocialApp(Flask, RoutedMixin):
if current_user.is_authenticated and \
not current_user.profile and \
request.endpoint != 'account.first_steps':
return redirect(url_for('account.first_steps'))
request.endpoint != 'first_steps':
return redirect(url_for('first_steps'))
return None
@ -129,6 +128,9 @@ class CalendarSocialApp(Flask, RoutedMixin):
"""The default time zone of the app
"""
from warnings import warn
from flask import has_app_context
from pytz import timezone, utc
from pytz.exceptions import UnknownTimeZoneError
@ -147,88 +149,61 @@ class CalendarSocialApp(Flask, RoutedMixin):
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
def _current_calendar():
from .calendar_system.gregorian import GregorianCalendar
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):
@route('/')
def hello():
"""View for the main page
This will display a welcome message for users not logged in; for others, their main
calendar view is displayed.
"""
calendar = self._current_calendar()
from .calendar_system.gregorian import GregorianCalendar
if not current_user.is_authenticated:
return self.about()
return render_template('welcome.html')
try:
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
except TypeError:
timestamp = datetime.utcnow()
calendar = GregorianCalendar(timestamp.timestamp())
return render_template('index.html', calendar=calendar, user_only=True)
@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
def new_event():
"""View for creating a new event
@ -253,7 +228,28 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('event-edit.html', form=form)
@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):
"""View to display event details
"""
@ -280,7 +276,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('event-details.html', event=event, form=form)
@staticmethod
@RoutedMixin.route('/profile/@<string:username>')
@route('/profile/@<string:username>')
def display_profile(username):
"""View to display profile details
"""
@ -295,7 +291,7 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('profile-details.html', profile=profile)
@staticmethod
@RoutedMixin.route('/profile/@<string:username>/follow')
@route('/profile/@<string:username>/follow')
@login_required
def follow_user(username):
"""View for following a user
@ -316,7 +312,22 @@ class CalendarSocialApp(Flask, RoutedMixin):
return redirect(url_for('display_profile', username=username))
@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):
"""View to accept an invitation
"""
@ -346,7 +357,55 @@ class CalendarSocialApp(Flask, RoutedMixin):
return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid))
@staticmethod
@RoutedMixin.route('/all-events')
@route('/first-steps', methods=['GET', 'POST'])
@login_required
def first_steps():
"""View to set up a new registrants profile
"""
from .forms import FirstStepsForm
from .models import db, Profile
if current_user.profile:
return redirect(url_for('hello'))
form = FirstStepsForm()
if form.validate_on_submit():
profile = Profile(user=current_user, display_name=form.display_name.data)
db.session.add(profile)
current_user.settings['timezone'] = str(form.time_zone.data)
db.session.commit()
return redirect(url_for('hello'))
return render_template('first-steps.html', form=form)
@staticmethod
@route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
"""View for editing ones profile
"""
from .forms import ProfileForm
from .models import db
form = ProfileForm(current_user.profile)
if form.validate_on_submit():
form.populate_obj(current_user.profile)
db.session.add(current_user.profile)
db.session.commit()
return redirect(url_for('edit_profile'))
return render_template('profile-edit.html', form=form)
@staticmethod
@route('/all-events')
def all_events():
"""View for listing all available events
"""
@ -362,5 +421,45 @@ class CalendarSocialApp(Flask, RoutedMixin):
return render_template('index.html', calendar=calendar, user_only=False)
@staticmethod
@route('/follow-requests')
@login_required
def follow_requests():
"""View for listing follow requests
"""
from .models import UserFollow
requests = UserFollow.query \
.filter(UserFollow.followed == current_user.profile) \
.filter(UserFollow.accepted_at.is_(None))
return render_template('follow-requests.html', requests=requests)
@staticmethod
@route('/follow-request/<int:follower_id>/accept')
@login_required
def accept_follow(follower_id):
"""View for accepting a follow request
"""
from .models import db, UserFollow
try:
req = UserFollow.query \
.filter(UserFollow.followed == current_user.profile) \
.filter(UserFollow.follower_id == follower_id) \
.one()
except NoResultFound:
abort(404)
if req.accepted_at is None:
req.accept()
db.session.add(req)
db.session.commit()
return redirect(url_for('follow_requests'))
app = CalendarSocialApp(__name__)

View File

@ -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 functools import wraps
from flask_babelex import lazy_gettext as _
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):
"""Gregorian calendar system for Calendar.social
"""
@ -71,27 +83,22 @@ class GregorianCalendar(CalendarSystem):
def days(self):
day_list = []
month_first = self.timestamp.replace(day=1)
start_day = self.timestamp.replace(day=1)
if self.timestamp.month == 12:
month_last = month_first.replace(day=31)
else:
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
while start_day.weekday() > self.START_DAY:
start_day -= timedelta(days=1)
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7
day_list.append(start_day)
current_day = start_day
first_display = month_first - timedelta(days=pad_before)
last_display = month_last + timedelta(days=pad_after)
current = first_display
while current <= last_display:
day_list.append(current)
current += timedelta(days=1)
while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
current_day += timedelta(days=1)
day_list.append(current_day)
return day_list
@property
@to_timestamp
def prev_year(self):
"""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
@property
@to_timestamp
def prev_month(self):
"""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]
@property
@to_timestamp
def next_month(self):
"""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]
@property
@to_timestamp
def next_year(self):
"""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)
return month_start_timestamp <= now < month_end_timestamp
return now >= month_start_timestamp and now < month_end_timestamp
@staticmethod
def day_events(date, user=None):
"""Returns all events for a given day
"""
from ..models import Event, EventVisibility, Invitation, Profile, Response
from ..models import Event, Profile
events = Event.query
if user:
events = events.outerjoin(Invitation) \
.outerjoin(Response) \
.join(Profile, Event.profile) \
events = events.join(Profile) \
.filter(Profile.user == user)
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
end_timestamp = start_timestamp + timedelta(days=1)
events = events.filter((Event.start_time <= end_timestamp) &
(Event.end_time >= start_timestamp)) \
events = events.filter(((Event.start_time >= start_timestamp) &
(Event.start_time < end_timestamp)) |
((Event.end_time >= start_timestamp) &
(Event.end_time < end_timestamp))) \
.order_by('start_time', 'end_time')
if user is None:
events = events.filter(Event.visibility == EventVisibility.public)
else:
events = events.filter((Event.visibility == EventVisibility.public) |
(Event.profile == user.profile) |
(Invitation.invitee == user.profile) |
(Response.profile == user.profile))
return events

View File

@ -1,7 +1,7 @@
"""Configuration file for the development environment
"""
ENV = 'development'
ENV = 'dev'
#: If ``True``, registration on the site is enabled.
REGISTRATION_ENABLED = True
#: The default time zone
@ -14,4 +14,3 @@ SECRET_KEY = 'ThisIsNotSoSecret'
SECURITY_PASSWORD_HASH = 'bcrypt'
SECURITY_PASSWORD_SALT = SECRET_KEY
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
"""
from enum import Enum
from flask_babelex import lazy_gettext as _
from flask_security.forms import LoginForm as BaseLoginForm
from flask_wtf import FlaskForm
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.validators import DataRequired, Email, StopValidation, ValidationError
from wtforms.widgets import TextArea
from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS
class UsernameAvailable: # pylint: disable=too-few-public-methods
class UsernameAvailable(object): # pylint: disable=too-few-public-methods
"""Checks if a username is available
"""
@ -62,7 +58,7 @@ class UsernameAvailable: # pylint: disable=too-few-public-methods
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
"""
@ -173,45 +169,6 @@ class TimezoneField(SelectField):
yield (value, label, value == self.data)
class EnumField(SelectField):
"""Field that allows selecting one value from an ``Enum`` class
:param enum_type: an ``Enum`` type
:type enum_type: type(Enum)
:param translations: translatable labels for enum values
:type translations: dict
:param args: passed verbatim to the constructor of `SelectField`
:param kwargs: passed verbatim to the constructor of `SelectField`
"""
def __init__(self, enum_type, translations, *args, **kwargs):
if not issubclass(enum_type, Enum):
raise TypeError('enum_type must be a subclass of Enum')
kwargs.update({'choices': [(value, None) for value in enum_type]})
self.data = None
self.enum_type = enum_type
self.translations = translations
SelectField.__init__(self, *args, **kwargs)
def process_formdata(self, valuelist):
if not valuelist:
self.data = None
return
try:
self.data = self.enum_type[valuelist[0]]
except KeyError:
raise ValueError('Unknown value')
def iter_choices(self):
for value in self.enum_type:
label = self.gettext(self.translations[value]) if self.translations else value.name
yield (value.name, label, value == self.data)
class EventForm(FlaskForm):
"""Form for event creation/editing
"""
@ -222,14 +179,6 @@ class EventForm(FlaskForm):
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
all_day = BooleanField(_('All day'))
description = StringField(_('Description'), widget=TextArea())
visibility = EnumField(EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, label=_('Visibility'))
def __init__(self, *args, **kwargs):
from flask_security import current_user
self.time_zone.kwargs['default'] = current_user.timezone # pylint: disable=no-member
FlaskForm.__init__(self, *args, **kwargs)
def populate_obj(self, obj):
"""Populate ``obj`` with event data
@ -394,20 +343,11 @@ class ProfileForm(FlaskForm):
"""
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,
})
kwargs.update({'display_name': profile.display_name})
kwargs.update({'locked': profile.locked})
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 warnings import warn
from flask import current_app
from flask_babelex import lazy_gettext
from flask_security import UserMixin, RoleMixin
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy_utils.types.choice import ChoiceType
from .app_state import app_state_base
from .cache import cache
from .utils import force_locale
db = SQLAlchemy()
@ -101,28 +98,29 @@ class ResponseType(Enum):
return self.name.lower() == other.lower() # pylint: disable=no-member
if isinstance(other, (int, float)):
return self.value == other # pylint: disable=comparison-with-callable
return self.value == other
return Enum.__eq__(self, other)
class EventVisibility(Enum):
"""Enumeration for event visibility
"""
class EventAvailability(Enum):
free = 0
busy = 1
#: The event is private, only attendees and people invited can see the details
class UserAvailability(EventAvailability):
tentative = 2
class ResponseVisibility(Enum):
private = 0
#: The event is public, anyone can see the details
organisers = 1
attendees = 2
followers = 3
friends = 4
public = 5
EVENT_VISIBILITY_TRANSLATIONS = {
EventVisibility.private: _('Visible only to attendees'),
EventVisibility.public: _('Visible to everyone'),
}
class SettingsProxy:
"""Proxy object to get settings for a user
"""
@ -208,6 +206,7 @@ class User(db.Model, UserMixin):
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.exceptions import UnknownTimeZoneError
@ -221,24 +220,6 @@ class User(db.Model, UserMixin):
return current_app.timezone
@property
def session_list_key(self):
"""The cache key of this users session list
"""
return f'open_sessions:{self.id}'
@property
def active_sessions(self):
"""The list of active sessions of this user
"""
return cache.get(self.session_list_key) or []
@active_sessions.setter
def active_sessions(self, value):
cache.set(self.session_list_key, list(value))
def __repr__(self):
return f'<User {self.id}({self.username})>'
@ -286,9 +267,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
#: 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
def fqn(self):
"""The fully qualified name of the profile
@ -439,9 +417,6 @@ class Event(db.Model):
#: The description of the event
description = db.Column(db.UnicodeText())
#: The visibility of the event
visibility = db.Column(db.Enum(EventVisibility), nullable=False)
def __as_tz(self, timestamp, as_timezone=None):
from pytz import timezone, utc
@ -790,64 +765,3 @@ class Response(db.Model): # pylint: disable=too-few-public-methods
#: The response itself
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
"""
from flask import current_app, session
from flask import current_app
from flask_login.signals import user_logged_in, user_logged_out
from flask_security import Security, AnonymousUser as BaseAnonymousUser
@ -35,15 +35,6 @@ class AnonymousUser(BaseAnonymousUser):
return current_app.timezone
@property
def profile(self):
"""The profile of the anonymous user
Always evaluates to ``None``
"""
return None
@user_logged_in.connect
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)
user.active_sessions += [session.sid]
@user_logged_out.connect
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-96.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}">
{% block head %}
<style>
header > h1 > img {
height: 1em;
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
{% block head %}{% endblock %}
footer {
margin-top: 3em;
font-weight: bold;
border-top: 1px dotted black;
padding-top: 1em;
}
</style>
{% endblock %}
</head>
<body>
<header>
<div class="ui top attached menu">
<div class="header item">
<h1>
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
Calendar.social
</div>
<div class="right menu">
{% if not current_user.is_authenticated %}
<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>
</h1>
<nav class="menu">
{% if current_user.is_authenticated %}
{{ _('Logged in as %(username)s', username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe) }}
{% endif %}
</div>
</div>
<ul>
{% 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>
<div class="ui container" id="content">
{% block content %}{% endblock %}
</div>
<footer class="ui segment">
<a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br>
<footer>
Soon…™
</footer>
{% block scripts %}{% endblock %}
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='semantic/semantic.min.js') }}"></script>
</body>
</html>

View File

@ -1,29 +1,15 @@
{% 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 %}
<h2 class="ui header">
<div class="content">
<h1>
{{ event.title }}<br>
<div class="sub header">
{%- if current_user.timezone | string != event.time_zone -%}
<span title="{{ time_zone_warning() }}">
<i class="fa fa-exclamation-triangle timezone-warning"></i>
<span class="sr-only">{{ time_zone_warning() }}</span>
</span>
<small>
{{ event.start_time_for_user(current_user) }}{{ event.end_time_for_user(current_user) }}
{% if current_user.timezone | string != event.time_zone %}
({{ event.start_time_tz }}{{ event.end_time_tz }} {{ event.time_zone }})
{% endif %}
{{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }}
{{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }}
</div>
</div>
</h2>
</small>
</h1>
{{ event.description }}
<hr>
<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>
<hr>
<h2>{% trans %}Invite{% endtrans %}</h2>
<form method="post" class="ui form">
<form method="post">
{{ form.hidden_tag() }}
<div class="inline fields">
{{ field(form.invitee, inline=true) }}
<button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button>
</div>
{{ form.invitee.errors }}
{{ form.invitee.label }}
{{ form.invitee}}
<button type="submit">{% trans %}Invite{% endtrans %}</button>
</form>
{% endblock %}

View File

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

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<h2>{% trans %}Follow requests{% endtrans %}</h2>
<ul>
{% for req in requests %}
<li>
{{ req.follower }}
<a href="{{ url_for('accept_follow', follower_id=req.follower_id) }}">{% trans %}Accept{% endtrans %}</a>
</li>
{% endfor %}
</ul>
{% endblock content %}

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% block content %}
{% trans username=current_user.username -%}
Welcome to Calendar.social, {{username}}!
{%- endtrans %}
{{ _('Welcome to Calendar.social, %(username)s!', username=current_user.username) }}
{% 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 %}

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">
<thead>
<tr class="sizer">
@ -11,10 +77,10 @@
</tr>
<tr class="month">
<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>
<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 colspan="3" class="month-name">
{% if not calendar.has_today %}
@ -26,10 +92,10 @@
{% endif %}
</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>
<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>
</tr>
<tr class="days">
@ -56,9 +122,7 @@
<span class="day-num">{{ day.day }}</span>
{% for event in calendar.day_events(day, user=current_user if user_only else none) %}
<a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event">
{% if not event.all_day %}
{{ event.start_time_for_user(current_user).strftime('%H:%M') }}{{ event.end_time_for_user(current_user).strftime('%H:%M') }}
{% endif %}
{{ event.title }}
</a>
{% endfor %}

View File

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

View File

@ -1,18 +1,13 @@
{% extends 'base.html' %}
{% from '_macros.html' import profile_link %}
{% block content %}
<h2 class="ui header">
{% if profile.builtin_avatar %}
<img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular image">
{% endif %}
<h1>
{% 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>
[locked]
{% endif %}
{{ profile.display_name }}
<small>@{{ profile.user.username}}</small>
</h2>
</h1>
{% if profile.user != current_user %}
<a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a>
{% endif %}
@ -22,7 +17,7 @@
</h2>
{% for followed in profile.followed_list %}
{{ profile_link(followed) }}
{{ followed }}
{% endfor %}
<h2>
@ -30,6 +25,6 @@
</h2>
{% for follower in profile.follower_list %}
{{ profile_link(follower) }}
{{ follower }}
{% endfor %}
{% endblock content %}

View File

@ -0,0 +1,22 @@
{% extends 'settings-base.html' %}
{% block content %}
{{ super() }}
<h2>{% trans %}Edit profile{% endtrans %}</h2>
<form method="post">
{{ form.hidden_tag() }}
{{ form.errors }}
{{ form.display_name.errors }}
{{ form.display_name.label }}
{{ form.display_name }}
<br>
{{ form.locked.errors }}
{{ form.locked.label }}
{{ form.locked}}
<br>
<button type="submit">{% trans %}Save{% endtrans %}</button>
</form>
{% endblock content %}

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,10 @@
{% extends 'base.html' %}
{% block content %}
<nav>
<ul>
<li><a href="{{ url_for('edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a></li>
<li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li>
</ul>
</nav>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'settings-base.html' %}
{% block content %}
{{ super() }}
<h2>{% trans %}Settings{% endtrans %}</h2>
<form method="post">
{{ form.hidden_tag() }}
{{ form.errors }}
<br>
{{ form.timezone.errors }}
{{ form.timezone.label }}
{{ form.timezone}}
<br>
<button type="submit">{% trans %}Save{% endtrans %}</button>
<a href="{{ url_for('hello') }}">Cancel</a>
</form>
{% endblock content %}

View File

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

View File

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

View File

@ -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
for key, value in orig_attrs.items():
setattr(ctx, key, value)
class RoutedMixin:
"""Mixin to lazily register class methods as routes
Works both for `Flask` and `Blueprint` objects.
Example::
class MyBlueprint(Blueprint, RoutedMixin):
def __init__(self, *args, **kwargs):
do_whatever_you_like()
RoutedMixin.register_routes(self)
@RoutedMixin.route('/')
def index(self):
return 'Hello, World!'
"""
def register_routes(self):
"""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

@ -14,10 +14,10 @@
# 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
"""Helper functions and fixtures for testing
"""
from contextlib import contextmanager
import pytest
import calsocial
from calsocial.models import db
@ -32,6 +32,22 @@ def configure_app():
calsocial.app.config['WTF_CSRF_ENABLED'] = False
@pytest.fixture
def client():
"""Fixture that provides a Flask test client
"""
configure_app()
client = calsocial.app.test_client()
with calsocial.app.app_context():
db.create_all()
yield client
with calsocial.app.app_context():
db.drop_all()
def login(client, username, password, no_redirect=False):
"""Login with the specified username and password
"""
@ -41,20 +57,16 @@ def login(client, username, password, no_redirect=False):
follow_redirects=not no_redirect)
@contextmanager
def alter_config(app, **kwargs):
saved = {}
@pytest.fixture
def database():
"""Fixture to provide all database tables in an active application context
"""
for key, value in kwargs.items():
if key in app.config:
saved[key] = app.config[key]
configure_app()
app.config[key] = value
with calsocial.app.app_context():
db.create_all()
yield
yield db
for key, value in kwargs.items():
if key in saved:
app.config[key] = saved[key]
else:
del app.config[key]
db.drop_all()

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

@ -17,60 +17,12 @@
"""General tests for Calendar.social
"""
from flask import current_app
from helpers import client
import pytest
def test_index_no_login(client):
"""Test the main page without logging in
"""
page = client.get('/')
assert b'Peek inside' in page.data
def test_instance_adin_unset(database):
"""Test the instance admin feature if the admin is not set
"""
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'):
assert current_app.instance_admin is None
def test_instance_admin_bad_value(database):
"""Test the instance admin feature if the value is invalid
"""
from calsocial.models import AppState
AppState['instance_admin'] = 'value'
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'):
assert current_app.instance_admin is None
def test_instance_admin_doesnot_exist(database):
"""Test the instance admin feature if the admin user does not exist
"""
from calsocial.models import AppState
AppState['instance_admin'] = '0'
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'):
assert current_app.instance_admin is None
def test_instance_admin(database):
"""Test the instance admin feature if the admin user does not exist
"""
from calsocial.models import db, AppState, User
user = User(username='admin')
db.session.add(user)
db.session.commit()
AppState['instance_admin'] = user.id
assert current_app.instance_admin == user
assert b'Welcome to Calendar.social' in page.data

View File

@ -20,7 +20,7 @@
import calsocial
from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow
from helpers import login
from helpers import client, database, login
def test_profile_follow(database):

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

@ -20,7 +20,15 @@
import calsocial
from calsocial.models import db, User
from helpers import login
from helpers import client, login
def test_index_no_login(client):
"""Test the main page without logging in
"""
page = client.get('/')
assert b'Welcome to Calendar.social' in page.data
def test_login_invalid_user(client):
@ -73,4 +81,4 @@ def test_login_first_steps(client):
assert page.location == 'http://localhost/'
page = client.get('/')
assert page.location == 'http://localhost/accounts/first-steps'
assert page.location == 'http://localhost/first-steps'

View File

@ -20,28 +20,28 @@
import calsocial
from calsocial.models import db, User
from helpers import alter_config
from helpers import client
def test_register_page(client):
"""Test the registration page
"""
page = client.get('/accounts/register')
page = client.get('/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={})
page = client.post('/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={
page = client.post('/register', data={
'username': 'test',
'email': 'test',
'password': 'password',
@ -53,7 +53,7 @@ def test_register_password_mismatch(client):
"""Test sending different password for registration
"""
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
@ -65,12 +65,13 @@ def test_register(client):
"""Test user registration
"""
page = client.post('/accounts/register', data={
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/'
@ -89,7 +90,7 @@ def test_register_existing_username(client):
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
page = client.post('/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
@ -106,16 +107,10 @@ def test_register_existing_email(client):
db.session.add(user)
db.session.commit()
page = client.post('/accounts/register', data={
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
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