Compare commits

...

75 Commits

Author SHA1 Message Date
7e7bb184ad Carica file su 'calsocial/translations/it/LC_MESSAGES'
Added Italian Translation
2018-09-14 14:48:27 +00:00
d635377d12 i18n: Add Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-07-30 22:28:26 +02:00
eaf71d4ce6 Add an empty profile for anonymous users
It evaluates to `None`, but at least it doesn’t raise an `AttributeError` now.
2018-07-26 21:50:12 +02:00
7cd2156cfc PyLint happiness (again) 2018-07-25 21:19:35 +02:00
b9c037f914 Display the instance admin on the welcome page 2018-07-25 21:07:47 +02:00
029d29ffb1 Make it possible to set the instance admin 2018-07-25 21:03:34 +02:00
4b1fff6544 Add the AppState model
This allows setting application state during run time
2018-07-25 20:27:13 +02:00
490474b2d6 Create a testing configuration
From now on, testing should be done with FLASK_ENV set to testing
2018-07-25 20:26:40 +02:00
bc67e692e0 [Refactor] Move test fixtures to conftest.py
…so they get automatically loaded
2018-07-25 20:25:12 +02:00
1e1e085ba4 [Bugfix] Use FLASK_ENV for the config filename instead of ENV 2018-07-25 20:25:12 +02:00
5996ae7079 Create a macro for profile links and use it on the welcome and profile display pages 2018-07-25 08:36:03 +02:00
3e5d8ee4d5 Make it possible to set one’s avatar
Only built-in avatars are supported yet.
2018-07-25 08:36:03 +02:00
c0c38ccb52 Make it possible to use builtin avatars
Images are from openclipart.org
2018-07-25 08:24:10 +02:00
c40e776036 Fully cover the Gregorian Calendar with tests
Being one of the cornerstones right now, it deserves a lot of tests
2018-07-24 08:28:04 +02:00
3deaa39256 [Test] Add test for disabled registration 2018-07-23 13:22:50 +02:00
c20b302458 Add the alter_config context manager for testing
It can temporarily change an app configuration value.
2018-07-23 13:22:50 +02:00
6f186c3a3f Add coverage related files to .gitignore 2018-07-23 13:22:50 +02:00
a97d884f42 Add pytest-cov as a developer dependency 2018-07-23 13:04:04 +02:00
11bd30e01f [Bugfix] Fix broken tests 2018-07-23 13:04:04 +02:00
4c3ec0564f [Bugfix] Fix registration
Allow POSTing to the registration endpoint
2018-07-23 13:04:04 +02:00
f8e3c748c0 [Bugfix] Fix jumping to first steps page upon initial login
This was missing from when the first steps view got moved to the accounts blueprint.
2018-07-23 13:04:04 +02:00
9e7ea29f5e [Lint] Make PyLint happy again 2018-07-23 12:35:57 +02:00
4935e6394b [Development] Ignore the .env file
It is used by pipenv, and we definitely don’t want to accidentally commit this to the repo.
2018-07-23 11:53:56 +02:00
2c01939ef5 [Docs] Add docstring to User.active_sessions 2018-07-23 11:53:56 +02:00
e45726fd7c [Refactor] Make the session list a list comprehension in account.py 2018-07-23 11:53:56 +02:00
26d58daac4 [Code Cleanup] Wrap a long line in accounts.py 2018-07-23 11:53:56 +02:00
387b7d83ac [Bugfix] Fix the environment name for dev mode
It turns out Flask only considers `development` as the development mode; `dev` (used before) is
not working.
2018-07-23 11:53:56 +02:00
9b27491652 [Bugfix] Import gettext in account.py 2018-07-23 11:53:43 +02:00
6078e6171f [Bugfix] Rework month padding and event fetching routines
Month padding (ie. adding the days of previous/next months) is now working as expected.  Fetching
multi-day events now also displays events correctly.
2018-07-23 08:00:06 +02:00
8eb52ff7f4 Hide time values for all-day events 2018-07-21 06:50:17 +02:00
cb9a62cd88 Make it possible to list and invalidate active sessions 2018-07-19 15:15:49 +02:00
8d71edae5e Save sessions in the cache 2018-07-19 15:15:49 +02:00
6c98c9d7ca Add caching functionality via Flask-Caching 2018-07-19 15:15:49 +02:00
bcb7b524f3 Move account related views to a separate blueprint 2018-07-19 15:15:49 +02:00
8d45611e35 Create the RoutedMixin class
It will be used both in the app, and later blueprint classes.
2018-07-17 15:00:56 +02:00
89dc258a5b [Bugfix] Fix the link of the “Go to your calendar” button on the about page 2018-07-17 12:39:09 +02:00
c90b261de3 [Refactor] Refactor the about page
This makes it available to logged in users, too.
2018-07-17 10:06:46 +02:00
372a1f756a Update the welcome page with actual numbers 2018-07-17 09:52:50 +02:00
43a90a237f Document the EnumField form field type 2018-07-16 13:34:05 +02:00
a763662cd6 Make sure the EnumField gets an Enum subclass as its parameter 2018-07-16 13:34:05 +02:00
41b4b9d7ea Fix the label of the EnumField field type
If there are no translations provided, use the enum name
2018-07-16 13:34:05 +02:00
64c72b1a68 Make PyLint happy 2018-07-16 13:34:05 +02:00
d36817ca44 Make the time zone on the event creation form default to the user’s time zone 2018-07-16 12:37:32 +02:00
a862e6ca5d Add the Event.visibility field
This shows if the event is visible to anyone or just people who are invited.

The calendar view already respects this flag.
2018-07-16 12:12:35 +02:00
f2f7ef72dd Update Hungarian translations 2018-07-16 11:12:56 +02:00
808c6bbdde Update translatable strings
Stop using the `_()` function, and use `{% trans %}` tags instead.
2018-07-16 11:09:09 +02:00
496b638694 [Bugfix] Add a missing closing tag to event-details.html 2018-07-16 10:42:10 +02:00
ff304dc64d Create the contents of the welcome page 2018-07-15 07:31:49 +02:00
13e55e7c68 [Cleanup] Style fix in style.css 2018-07-15 07:18:50 +02:00
b54674c703 Semantic UI version 2018-07-13 16:03:33 +02:00
b82cacc665 Make locked profiles display a lock icon instead of the text “locked”
This involves adding [ForkAwesome](https://forkawesome.github.io/) to the dependencies, even
though it is pulled in via a CDN.
2018-07-13 15:33:08 +02:00
d06cfaa02e Add a field macro to the event creation form 2018-07-13 15:33:06 +02:00
a133218906 Add necessary META tags
Charset and viewport
2018-07-13 09:34:56 +02:00
0714474dc6 Move site CSS to a separate file 2018-07-13 08:58:12 +02:00
3308be40ee Display the locked status of profiles on the profile details page 2018-07-12 12:18:51 +02:00
9b01431641 Make it possible for users to lock their profiles on the profile editing page 2018-07-12 12:17:18 +02:00
2b1378310a [Bugfix] Make the profile editing form redirect to the profile editing form after saving
As funny as it says, this provides better UX (and the same technique is used on the other forms.
2018-07-12 12:16:22 +02:00
5639c3f578 Make it possible to accept follow requests 2018-07-12 12:10:53 +02:00
61f10f951c Make it possible to lock profiles
Locked profiles cannot be followed
2018-07-12 12:10:53 +02:00
496b5b6c04 [Refactor] Move notification creation to the Profile model 2018-07-12 12:00:36 +02:00
dc0b2954c1 [Refactor] Move invitation code to the Event model 2018-07-12 11:08:13 +02:00
36c2f0fd77 Add test for following a user through the web UI 2018-07-12 10:34:59 +02:00
27c78ff36f Create the Profile.follow() method 2018-07-12 10:34:59 +02:00
37e08fed22 Add tests for logging in 2018-07-12 10:34:57 +02:00
a0fba3f2af Move registration related tests to a separate file 2018-07-12 10:32:16 +02:00
48a19a2296 Update tests
Move the `client` fixture to the helpers module, as it will be used by many other test cases.

Add the AGPL header to the test files.
2018-07-12 10:28:59 +02:00
5d886a7853 [Bugfix] Fix the FQN of invalid remote profiles (ie. no domain set) 2018-07-12 10:17:56 +02:00
5550e5ecf3 Make PyLint happy again 2018-07-12 10:16:04 +02:00
0a3cfafef3 Fix .gitignore rule for .mo files 2018-07-12 09:13:24 +02:00
8e3bcd8ede Create a view that can list all events 2018-07-11 12:57:32 +02:00
48ffb0d472 [Refactor] Refactor the month view template so it can display all events
…not just the events of the current user.
2018-07-11 12:57:32 +02:00
c3348d3212 Make it possible to edit one’s profile
…even though it’s only one field yet.
2018-07-11 12:57:32 +02:00
1a69928241 [Refactor] Create a base settings page
This will make it easier to add new settings pages for e.g. privacy settings, applications,
etc. later.
2018-07-11 12:57:32 +02:00
7b935afdad [Bugfix] Fix the event creating code
It tried to associate the event with a user instead of a profile.  It is a regression introduced
by #41
2018-07-11 12:57:32 +02:00
303dd3d082 [Bugfix] Fix the display name on the profile pages 2018-07-11 09:35:32 +02:00
55 changed files with 4111 additions and 554 deletions

1
.env.testing Normal file
View File

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

5
.gitignore vendored
View File

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

View File

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

View File

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

234
calsocial/account.py Normal file
View File

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

60
calsocial/app_state.py Normal file
View File

@ -0,0 +1,60 @@
# 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})

159
calsocial/cache.py Normal file
View File

@ -0,0 +1,159 @@
# 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,24 +18,12 @@
"""
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
"""
@ -83,22 +71,27 @@ class GregorianCalendar(CalendarSystem):
def days(self):
day_list = []
start_day = self.timestamp.replace(day=1)
month_first = self.timestamp.replace(day=1)
while start_day.weekday() > self.START_DAY:
start_day -= timedelta(days=1)
if self.timestamp.month == 12:
month_last = month_first.replace(day=31)
else:
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
day_list.append(start_day)
current_day = start_day
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7
while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
current_day += timedelta(days=1)
day_list.append(current_day)
first_display = month_first - timedelta(days=pad_before)
last_display = month_last + timedelta(days=pad_after)
current = first_display
while current <= last_display:
day_list.append(current)
current += timedelta(days=1)
return day_list
@property
@to_timestamp
def prev_year(self):
"""Returns the timestamp of the same date in the previous year
"""
@ -113,7 +106,6 @@ 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
"""
@ -136,7 +128,6 @@ 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
"""
@ -159,7 +150,6 @@ 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
"""
@ -192,28 +182,36 @@ class GregorianCalendar(CalendarSystem):
month_end_timestamp = month_start_timestamp.replace(month=next_month)
return now >= month_start_timestamp and now < month_end_timestamp
return month_start_timestamp <= now < month_end_timestamp
@staticmethod
def day_events(date, user=None):
"""Returns all events for a given day
"""
from ..models import Event, Profile
from ..models import Event, EventVisibility, Invitation, Profile, Response
events = Event.query
if user:
events = events.join(Profile) \
events = events.outerjoin(Invitation) \
.outerjoin(Response) \
.join(Profile, Event.profile) \
.filter(Profile.user == user)
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
end_timestamp = start_timestamp + timedelta(days=1)
events = events.filter(((Event.start_time >= start_timestamp) &
(Event.start_time < end_timestamp)) |
((Event.end_time >= start_timestamp) &
(Event.end_time < end_timestamp))) \
events = events.filter((Event.start_time <= end_timestamp) &
(Event.end_time >= start_timestamp)) \
.order_by('start_time', 'end_time')
if user is None:
events = events.filter(Event.visibility == EventVisibility.public)
else:
events = events.filter((Event.visibility == EventVisibility.public) |
(Event.profile == user.profile) |
(Invitation.invitee == user.profile) |
(Response.profile == user.profile))
return events

View File

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

View File

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

View File

@ -21,12 +21,15 @@ 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()
@ -98,11 +101,28 @@ class ResponseType(Enum):
return self.name.lower() == other.lower() # pylint: disable=no-member
if isinstance(other, (int, float)):
return self.value == other
return self.value == other # pylint: disable=comparison-with-callable
return Enum.__eq__(self, other)
class EventVisibility(Enum):
"""Enumeration for event visibility
"""
#: The event is private, only attendees and people invited can see the details
private = 0
#: The event is public, anyone can see the details
public = 5
EVENT_VISIBILITY_TRANSLATIONS = {
EventVisibility.private: _('Visible only to attendees'),
EventVisibility.public: _('Visible to everyone'),
}
class SettingsProxy:
"""Proxy object to get settings for a user
"""
@ -188,7 +208,6 @@ 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
@ -202,6 +221,24 @@ class User(db.Model, UserMixin):
return current_app.timezone
@property
def session_list_key(self):
"""The cache key of this users session list
"""
return f'open_sessions:{self.id}'
@property
def active_sessions(self):
"""The list of active sessions of this user
"""
return cache.get(self.session_list_key) or []
@active_sessions.setter
def active_sessions(self, value):
cache.set(self.session_list_key, list(value))
def __repr__(self):
return f'<User {self.id}({self.username})>'
@ -246,6 +283,12 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
#: The display name
display_name = db.Column(db.Unicode(length=80), nullable=False)
#: If locked, a profile cannot be followed without the owners consent
locked = db.Column(db.Boolean(), default=False)
#: If set, the profile will display this builtin avatar
builtin_avatar = db.Column(db.String(length=40), nullable=True)
@property
def fqn(self):
"""The fully qualified name of the profile
@ -265,7 +308,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
domain = ''
else:
username = self.username
domain = '@' + self.domain
domain = f'@{self.domain}'
return f'<Profile {self.id}(@{username}{domain})>'
@ -290,7 +333,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \
.join(UserFollow.followed) \
.filter(UserFollow.follower == self)
.filter(UserFollow.follower == self) \
.filter(UserFollow.accepted_at.isnot(None))
@property
def follower_list(self):
@ -303,7 +347,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \
.join(UserFollow.follower) \
.filter(UserFollow.followed == self)
.filter(UserFollow.followed == self) \
.filter(UserFollow.accepted_at.isnot(None))
@property
def url(self):
@ -317,6 +362,48 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return NotImplemented
def follow(self, follower):
"""Make ``follower`` follow this profile
"""
if not isinstance(follower, Profile):
raise TypeError('Folloer must be a Profile object')
timestamp = None if self.locked else datetime.utcnow()
user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp)
db.session.add(user_follow)
notification = self.notify(follower, self, NotificationAction.follow)
db.session.add(notification)
return user_follow
def notify(self, actor, item, action):
"""Notify this profile about ``action`` on ``item`` by ``actor``
:param actor: the actor who generated the notification
:type actor: Profile
:param item: the item ``action`` was performed on
:type item: any
:param action: the type of the action
:type action: NotificationAction, str
:raises TypeError: if ``actor`` is not a `Profile` object
:returns: the generated notification. It is already added to the database session, but
not committed
:rtype: Notification
"""
if not isinstance(actor, Profile):
raise TypeError('actor must be a Profile instance')
if isinstance(action, str):
action = NotificationAction[action]
notification = Notification(profile=self, actor=actor, item=item, action=action)
return notification
class Event(db.Model):
"""Database model for events
@ -352,6 +439,9 @@ class Event(db.Model):
#: The description of the event
description = db.Column(db.UnicodeText())
#: The visibility of the event
visibility = db.Column(db.Enum(EventVisibility), nullable=False)
def __as_tz(self, timestamp, as_timezone=None):
from pytz import timezone, utc
@ -400,6 +490,20 @@ class Event(db.Model):
return url_for('event_details', event_uuid=self.event_uuid)
def invite(self, inviter, invited):
"""Invite ``invited`` to the event
The invitation will arrive from ``inviter``.
"""
invite = Invitation(event=self, sender=inviter, invitee=invited)
db.session.add(invite)
notification = invited.notify(inviter, self, NotificationAction.invite)
db.session.add(notification)
return invite
class UserSetting(db.Model): # pylint: disable=too-few-public-methods
"""Database model for user settings
@ -543,6 +647,12 @@ class UserFollow(db.Model): # pylint: disable=too-few-public-methods
#: The timestamp when the follow was accepted
accepted_at = db.Column(db.DateTime(), nullable=True)
def accept(self):
"""Accept this follow request
"""
self.accepted_at = datetime.utcnow()
class Notification(db.Model):
"""Database model for notifications
@ -680,3 +790,64 @@ 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
from flask import current_app, session
from flask_login.signals import user_logged_in, user_logged_out
from flask_security import Security, AnonymousUser as BaseAnonymousUser
@ -35,6 +35,15 @@ 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
@ -45,6 +54,8 @@ def login_handler(app, user): # pylint: disable=unused-argument
AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS)
user.active_sessions += [session.sid]
@user_logged_out.connect
def logout_handler(app, user): # pylint: disable=unused-argument

View File

@ -0,0 +1,35 @@
<?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>

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,91 @@
<?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>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,39 @@
<?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>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,11 @@
<?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>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,26 @@
<?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>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
{% 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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,22 @@
{% 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,466 @@
# 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,3 +68,54 @@ 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

62
tests/conftest.py Normal file
View File

@ -0,0 +1,62 @@
# 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()

60
tests/helpers.py Normal file
View File

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

49
tests/test_app_state.py Normal file
View File

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

View File

@ -1,121 +1,76 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""General tests for Calendar.social
"""
from flask import current_app
import pytest
import calsocial
from calsocial.models import db, User
@pytest.fixture
def client():
calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
calsocial.app.config['TESTING'] = True
calsocial.app.config['WTF_CSRF_ENABLED'] = False
client = calsocial.app.test_client()
with calsocial.app.app_context():
db.create_all()
yield client
with calsocial.app.app_context():
db.drop_all()
def test_index_no_login(client):
"""Test the main page without logging in
"""
page = client.get('/')
assert b'Welcome to Calendar.social' in page.data
assert b'Peek inside' in page.data
def test_register_page(client):
"""Test the registration page
def test_instance_adin_unset(database):
"""Test the instance admin feature if the admin is not set
"""
page = client.get('/register')
assert b'Register</button>' in page.data
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'):
assert current_app.instance_admin is None
def test_register_post_empty(client):
"""Test sending empty registration data
def test_instance_admin_bad_value(database):
"""Test the instance admin feature if the value is invalid
"""
page = client.post('/register', data={})
assert b'This field is required' in page.data
from calsocial.models import AppState
def test_register_invalid_email(client):
"""Test sending an invalid email address
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
"""
page = client.post('/register', data={
'username': 'test',
'email': 'test',
'password': 'password',
'password_retype': 'password',
})
assert b'Invalid email address' in page.data
from calsocial.models import AppState
def test_register_password_mismatch(client):
"""Test sending different password for registration
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
"""
page = client.post('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'something',
})
assert b'The two passwords must match' in page.data
from calsocial.models import db, AppState, User
def test_register(client):
"""Test user registration
"""
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/'
with calsocial.app.app_context():
user = User.query.one()
assert user.username == 'test'
assert user.email == 'test@example.com'
def test_register_existing_username(client):
"""Test registering an existing username
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
user = User(username='admin')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This username is not available' in page.data
AppState['instance_admin'] = user.id
def test_register_existing_email(client):
"""Test registering an existing email address
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'tester',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This email address can not be used' in page.data
assert current_app.instance_admin == user

117
tests/test_follow.py Normal file
View File

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

92
tests/test_gregorian.py Normal file
View File

@ -0,0 +1,92 @@
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'

76
tests/test_login.py Normal file
View File

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

121
tests/test_register.py Normal file
View File

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