forked from gergely/calendar-social
		
	Compare commits
	
		
			75 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7e7bb184ad | |||
| d635377d12 | |||
| eaf71d4ce6 | |||
| 7cd2156cfc | |||
| b9c037f914 | |||
| 029d29ffb1 | |||
| 4b1fff6544 | |||
| 490474b2d6 | |||
| bc67e692e0 | |||
| 1e1e085ba4 | |||
| 5996ae7079 | |||
| 3e5d8ee4d5 | |||
| c0c38ccb52 | |||
| c40e776036 | |||
| 3deaa39256 | |||
| c20b302458 | |||
| 6f186c3a3f | |||
| a97d884f42 | |||
| 11bd30e01f | |||
| 4c3ec0564f | |||
| f8e3c748c0 | |||
| 9e7ea29f5e | |||
| 4935e6394b | |||
| 2c01939ef5 | |||
| e45726fd7c | |||
| 26d58daac4 | |||
| 387b7d83ac | |||
| 9b27491652 | |||
| 6078e6171f | |||
| 8eb52ff7f4 | |||
| cb9a62cd88 | |||
| 8d71edae5e | |||
| 6c98c9d7ca | |||
| bcb7b524f3 | |||
| 8d45611e35 | |||
| 89dc258a5b | |||
| c90b261de3 | |||
| 372a1f756a | |||
| 43a90a237f | |||
| a763662cd6 | |||
| 41b4b9d7ea | |||
| 64c72b1a68 | |||
| d36817ca44 | |||
| a862e6ca5d | |||
| f2f7ef72dd | |||
| 808c6bbdde | |||
| 496b638694 | |||
| ff304dc64d | |||
| 13e55e7c68 | |||
| b54674c703 | |||
| b82cacc665 | |||
| d06cfaa02e | |||
| a133218906 | |||
| 0714474dc6 | |||
| 3308be40ee | |||
| 9b01431641 | |||
| 2b1378310a | |||
| 5639c3f578 | |||
| 61f10f951c | |||
| 496b5b6c04 | |||
| dc0b2954c1 | |||
| 36c2f0fd77 | |||
| 27c78ff36f | |||
| 37e08fed22 | |||
| a0fba3f2af | |||
| 48a19a2296 | |||
| 5d886a7853 | |||
| 5550e5ecf3 | |||
| 0a3cfafef3 | |||
| 8e3bcd8ede | |||
| 48ffb0d472 | |||
| c3348d3212 | |||
| 1a69928241 | |||
| 7b935afdad | |||
| 303dd3d082 | 
							
								
								
									
										1
									
								
								.env.testing
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.testing
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | FLASK_ENV=testing | ||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,8 @@ | |||||||
| __pycache__/ | __pycache__/ | ||||||
| /calsocial/local.db | /calsocial/local.db | ||||||
| /messages.pot | /messages.pot | ||||||
| /app/translations/*/LC_MESSAGES/*.mo | /calsocial/translations/*/LC_MESSAGES/*.mo | ||||||
| /.pytest_cache/ | /.pytest_cache/ | ||||||
|  | /.env | ||||||
|  | /.coverage | ||||||
|  | /htmlcov/ | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -13,10 +13,12 @@ sqlalchemy-utils = "*" | |||||||
| bcrypt = "*" | bcrypt = "*" | ||||||
| flask-babelex = "*" | flask-babelex = "*" | ||||||
| python-dateutil = "*" | python-dateutil = "*" | ||||||
|  | flask-caching = "*" | ||||||
|  |  | ||||||
| [dev-packages] | [dev-packages] | ||||||
| pylint = "*" | pylint = "*" | ||||||
| pytest = "*" | pytest = "*" | ||||||
|  | pytest-cov = "*" | ||||||
|  |  | ||||||
| [requires] | [requires] | ||||||
| python_version = "3.6" | python_version = "3.6" | ||||||
|   | |||||||
							
								
								
									
										107
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										107
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7" |             "sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @@ -128,6 +128,14 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.9.3" |             "version": "==0.9.3" | ||||||
|         }, |         }, | ||||||
|  |         "flask-caching": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227", | ||||||
|  |                 "sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==1.4.0" | ||||||
|  |         }, | ||||||
|         "flask-login": { |         "flask-login": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" |                 "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" | ||||||
| @@ -247,9 +255,9 @@ | |||||||
|         }, |         }, | ||||||
|         "sqlalchemy": { |         "sqlalchemy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957" |                 "sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.2.9" |             "version": "==1.2.10" | ||||||
|         }, |         }, | ||||||
|         "sqlalchemy-utils": { |         "sqlalchemy-utils": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -276,10 +284,10 @@ | |||||||
|     "develop": { |     "develop": { | ||||||
|         "astroid": { |         "astroid": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a", |                 "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", | ||||||
|                 "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a" |                 "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.6.5" |             "version": "==2.0.1" | ||||||
|         }, |         }, | ||||||
|         "atomicwrites": { |         "atomicwrites": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -295,6 +303,50 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==18.1.0" |             "version": "==18.1.0" | ||||||
|         }, |         }, | ||||||
|  |         "coverage": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", | ||||||
|  |                 "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", | ||||||
|  |                 "sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", | ||||||
|  |                 "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", | ||||||
|  |                 "sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", | ||||||
|  |                 "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", | ||||||
|  |                 "sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", | ||||||
|  |                 "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", | ||||||
|  |                 "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", | ||||||
|  |                 "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", | ||||||
|  |                 "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", | ||||||
|  |                 "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", | ||||||
|  |                 "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", | ||||||
|  |                 "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", | ||||||
|  |                 "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", | ||||||
|  |                 "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", | ||||||
|  |                 "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", | ||||||
|  |                 "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", | ||||||
|  |                 "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", | ||||||
|  |                 "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", | ||||||
|  |                 "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", | ||||||
|  |                 "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", | ||||||
|  |                 "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", | ||||||
|  |                 "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", | ||||||
|  |                 "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", | ||||||
|  |                 "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", | ||||||
|  |                 "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", | ||||||
|  |                 "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", | ||||||
|  |                 "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", | ||||||
|  |                 "sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", | ||||||
|  |                 "sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", | ||||||
|  |                 "sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", | ||||||
|  |                 "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", | ||||||
|  |                 "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", | ||||||
|  |                 "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", | ||||||
|  |                 "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", | ||||||
|  |                 "sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", | ||||||
|  |                 "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", | ||||||
|  |                 "sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e" | ||||||
|  |             ], | ||||||
|  |             "version": "==4.5.1" | ||||||
|  |         }, | ||||||
|         "isort": { |         "isort": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", |                 "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", | ||||||
| @@ -369,11 +421,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c", |                 "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", | ||||||
|                 "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3" |                 "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.9.2" |             "version": "==2.0.1" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -383,6 +435,14 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.6.3" |             "version": "==3.6.3" | ||||||
|         }, |         }, | ||||||
|  |         "pytest-cov": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", | ||||||
|  |                 "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==2.5.1" | ||||||
|  |         }, | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", |                 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", | ||||||
| @@ -390,6 +450,35 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==1.11.0" |             "version": "==1.11.0" | ||||||
|         }, |         }, | ||||||
|  |         "typed-ast": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", | ||||||
|  |                 "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", | ||||||
|  |                 "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", | ||||||
|  |                 "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", | ||||||
|  |                 "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", | ||||||
|  |                 "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", | ||||||
|  |                 "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", | ||||||
|  |                 "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", | ||||||
|  |                 "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", | ||||||
|  |                 "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", | ||||||
|  |                 "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", | ||||||
|  |                 "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", | ||||||
|  |                 "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", | ||||||
|  |                 "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", | ||||||
|  |                 "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", | ||||||
|  |                 "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", | ||||||
|  |                 "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", | ||||||
|  |                 "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", | ||||||
|  |                 "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", | ||||||
|  |                 "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", | ||||||
|  |                 "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", | ||||||
|  |                 "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", | ||||||
|  |                 "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version < '3.7' and implementation_name == 'cpython'", | ||||||
|  |             "version": "==1.1.0" | ||||||
|  |         }, | ||||||
|         "wrapt": { |         "wrapt": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" |                 "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" | ||||||
|   | |||||||
| @@ -19,12 +19,18 @@ | |||||||
|  |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| import os | 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_babelex import Babel, get_locale as babel_get_locale | ||||||
| from flask_security import SQLAlchemyUserDatastore, current_user, login_required | from flask_security import SQLAlchemyUserDatastore, current_user, login_required | ||||||
| from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound | ||||||
|  |  | ||||||
|  | from calsocial.account import AccountBlueprint | ||||||
|  | from calsocial.cache import CachedSessionInterface, cache | ||||||
|  | from calsocial.utils import RoutedMixin | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_locale(): | def get_locale(): | ||||||
|     """Locale selector |     """Locale selector | ||||||
| @@ -53,22 +59,7 @@ def template_vars(): | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| def route(*args, **kwargs): | class CalendarSocialApp(Flask, RoutedMixin): | ||||||
|     """Mark a function as a future route |  | ||||||
|  |  | ||||||
|     Such functions will be iterated over when the application is initialised.  ``*args`` and |  | ||||||
|     ``**kwargs`` will be passed verbatim to `Flask.route()`. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def decorator(func):  # pylint: disable=missing-docstring |  | ||||||
|         setattr(func, 'routing', (args, kwargs)) |  | ||||||
|  |  | ||||||
|         return func |  | ||||||
|  |  | ||||||
|     return decorator |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CalendarSocialApp(Flask): |  | ||||||
|     """The Calendar.social app |     """The Calendar.social app | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
| @@ -79,13 +70,32 @@ class CalendarSocialApp(Flask): | |||||||
|  |  | ||||||
|         Flask.__init__(self, name) |         Flask.__init__(self, name) | ||||||
|  |  | ||||||
|  |         self.session_interface = CachedSessionInterface() | ||||||
|  |  | ||||||
|         self._timezone = None |         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) |         self.config.from_pyfile(f'config_{config_name}.py', True) | ||||||
|         # Make sure we look up users both by their usernames and email addresses |         # Make sure we look up users both by their usernames and email addresses | ||||||
|         self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') |         self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email') | ||||||
|  |         self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html' | ||||||
|  |  | ||||||
|  |         # The builtin avatars to use | ||||||
|  |         self.config['BUILTIN_AVATARS'] = ( | ||||||
|  |             'doctor', | ||||||
|  |             'engineer', | ||||||
|  |             'scientist', | ||||||
|  |             'statistician', | ||||||
|  |             'user', | ||||||
|  |             'whoami', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.jinja_env.policies['ext.i18n.trimmed'] = True  # pylint: disable=no-member | ||||||
|  |  | ||||||
|         db.init_app(self) |         db.init_app(self) | ||||||
|  |  | ||||||
|  |         cache.init_app(self) | ||||||
|  |  | ||||||
|         babel = Babel(app=self) |         babel = Babel(app=self) | ||||||
|         babel.localeselector(get_locale) |         babel.localeselector(get_locale) | ||||||
|  |  | ||||||
| @@ -96,18 +106,9 @@ class CalendarSocialApp(Flask): | |||||||
|  |  | ||||||
|         self.context_processor(template_vars) |         self.context_processor(template_vars) | ||||||
|  |  | ||||||
|         for attr_name in self.__dir__(): |         RoutedMixin.register_routes(self) | ||||||
|             attr = getattr(self, attr_name) |  | ||||||
|  |  | ||||||
|             if not callable(attr): |         AccountBlueprint().init_app(self, '/accounts/') | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             args, kwargs = getattr(attr, 'routing', (None, None)) |  | ||||||
|  |  | ||||||
|             if args is None: |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             self.route(*args, **kwargs)(attr) |  | ||||||
|  |  | ||||||
|         self.before_request(self.goto_first_steps) |         self.before_request(self.goto_first_steps) | ||||||
|  |  | ||||||
| @@ -118,8 +119,8 @@ class CalendarSocialApp(Flask): | |||||||
|  |  | ||||||
|         if current_user.is_authenticated and \ |         if current_user.is_authenticated and \ | ||||||
|            not current_user.profile and \ |            not current_user.profile and \ | ||||||
|            request.endpoint != 'first_steps': |            request.endpoint != 'account.first_steps': | ||||||
|             return redirect(url_for('first_steps')) |             return redirect(url_for('account.first_steps')) | ||||||
|  |  | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
| @@ -128,9 +129,6 @@ class CalendarSocialApp(Flask): | |||||||
|         """The default time zone of the app |         """The default time zone of the app | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from warnings import warn |  | ||||||
|  |  | ||||||
|         from flask import has_app_context |  | ||||||
|         from pytz import timezone, utc |         from pytz import timezone, utc | ||||||
|         from pytz.exceptions import UnknownTimeZoneError |         from pytz.exceptions import UnknownTimeZoneError | ||||||
|  |  | ||||||
| @@ -149,61 +147,88 @@ class CalendarSocialApp(Flask): | |||||||
|  |  | ||||||
|         return self._timezone |         return self._timezone | ||||||
|  |  | ||||||
|     @staticmethod |     @property | ||||||
|     @route('/') |     def instance_admin(self): | ||||||
|     def hello(): |         """The admin user of this instance | ||||||
|         """View for the main page |  | ||||||
|  |  | ||||||
|         This will display a welcome message for users not logged in; for others, their main |  | ||||||
|         calendar view is displayed. |  | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from .calendar_system.gregorian import GregorianCalendar |         from calsocial.models import AppState, User | ||||||
|  |  | ||||||
|         if not current_user.is_authenticated: |         if not has_app_context(): | ||||||
|             return render_template('welcome.html') |             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: |         try: | ||||||
|             timestamp = datetime.fromtimestamp(float(request.args.get('date'))) |             timestamp = datetime.fromtimestamp(float(request.args.get('date'))) | ||||||
|         except TypeError: |         except TypeError: | ||||||
|             timestamp = datetime.utcnow() |             timestamp = datetime.utcnow() | ||||||
|  |  | ||||||
|         calendar = GregorianCalendar(timestamp.timestamp()) |         return GregorianCalendar(timestamp.timestamp()) | ||||||
|  |  | ||||||
|         return render_template('index.html', calendar=calendar) |     @RoutedMixin.route('/about') | ||||||
|  |     def about(self): | ||||||
|     @staticmethod |         """View for the about page | ||||||
|     @route('/register', methods=['POST', 'GET']) |  | ||||||
|     def register(): |  | ||||||
|         """View for user registration |  | ||||||
|  |  | ||||||
|         If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the |  | ||||||
|         registration disabled template.  Otherwise, it performs user registration. |  | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if not current_app.config['REGISTRATION_ENABLED']: |         from .models import User, Event | ||||||
|             return render_template('registration-disabled.html') |  | ||||||
|  |  | ||||||
|         from .forms import RegistrationForm |         calendar = self._current_calendar() | ||||||
|         from .models import db, User |  | ||||||
|  |  | ||||||
|         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(): |         user_count = User.query.count() | ||||||
|             # TODO: This might become False later, if we want registrations to be confirmed via |         event_count = Event.query.count() | ||||||
|             #       e-mail |         admin_user = current_app.instance_admin | ||||||
|             user = User(active=True) |         admin_profile = None if admin_user is None else admin_user.profile | ||||||
|  |  | ||||||
|             form.populate_obj(user) |         return render_template('welcome.html', | ||||||
|             db.session.add(user) |                                calendar=calendar, | ||||||
|             db.session.commit() |                                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 |     @staticmethod | ||||||
|     @route('/new-event', methods=['GET', 'POST']) |     @RoutedMixin.route('/new-event', methods=['GET', 'POST']) | ||||||
|     @login_required |     @login_required | ||||||
|     def new_event(): |     def new_event(): | ||||||
|         """View for creating a new event |         """View for creating a new event | ||||||
| @@ -217,7 +242,7 @@ class CalendarSocialApp(Flask): | |||||||
|         form = EventForm() |         form = EventForm() | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |         if form.validate_on_submit(): | ||||||
|             event = Event(user=current_user) |             event = Event(profile=current_user.profile) | ||||||
|             form.populate_obj(event) |             form.populate_obj(event) | ||||||
|  |  | ||||||
|             db.session.add(event) |             db.session.add(event) | ||||||
| @@ -228,34 +253,13 @@ class CalendarSocialApp(Flask): | |||||||
|         return render_template('event-edit.html', form=form) |         return render_template('event-edit.html', form=form) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @route('/settings', methods=['GET', 'POST']) |     @RoutedMixin.route('/event/<string:event_uuid>', methods=['GET', 'POST']) | ||||||
|     @login_required |  | ||||||
|     def settings(): |  | ||||||
|         """View for user settings |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .forms import SettingsForm |  | ||||||
|         from .models import db |  | ||||||
|  |  | ||||||
|         form = SettingsForm(current_user) |  | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |  | ||||||
|             form.populate_obj(current_user) |  | ||||||
|  |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         return render_template('user-settings.html', form=form) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @route('/event/<string:event_uuid>', methods=['GET', 'POST']) |  | ||||||
|     def event_details(event_uuid): |     def event_details(event_uuid): | ||||||
|         """View to display event details |         """View to display event details | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from .forms import InviteForm |         from .forms import InviteForm | ||||||
|         from .models import db, Event, Invitation, Notification, NotificationAction |         from .models import db, Event | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             event = Event.query.filter(Event.event_uuid == event_uuid).one() |             event = Event.query.filter(Event.event_uuid == event_uuid).one() | ||||||
| @@ -267,15 +271,7 @@ class CalendarSocialApp(Flask): | |||||||
|         form = InviteForm(event) |         form = InviteForm(event) | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |         if form.validate_on_submit(): | ||||||
|             invite = Invitation(event=event, sender=current_user.profile) |             event.invite(current_user.profile, invitee=form.invitee.data) | ||||||
|             form.populate_obj(invite) |  | ||||||
|             db.session.add(invite) |  | ||||||
|  |  | ||||||
|             notification = Notification(profile=form.invitee.data, |  | ||||||
|                                         actor=current_user.profile, |  | ||||||
|                                         item=event, |  | ||||||
|                                         action=NotificationAction.invite) |  | ||||||
|             db.session.add(notification) |  | ||||||
|  |  | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
| @@ -284,7 +280,7 @@ class CalendarSocialApp(Flask): | |||||||
|         return render_template('event-details.html', event=event, form=form) |         return render_template('event-details.html', event=event, form=form) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @route('/profile/@<string:username>') |     @RoutedMixin.route('/profile/@<string:username>') | ||||||
|     def display_profile(username): |     def display_profile(username): | ||||||
|         """View to display profile details |         """View to display profile details | ||||||
|         """ |         """ | ||||||
| @@ -299,12 +295,13 @@ class CalendarSocialApp(Flask): | |||||||
|         return render_template('profile-details.html', profile=profile) |         return render_template('profile-details.html', profile=profile) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @route('/profile/@<string:username>/follow') |     @RoutedMixin.route('/profile/@<string:username>/follow') | ||||||
|  |     @login_required | ||||||
|     def follow_user(username): |     def follow_user(username): | ||||||
|         """View for following a user |         """View for following a user | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from .models import db, Profile, User, UserFollow, Notification, NotificationAction |         from .models import db, Profile, User | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             profile = Profile.query.join(User).filter(User.username == username).one() |             profile = Profile.query.join(User).filter(User.username == username).one() | ||||||
| @@ -312,38 +309,14 @@ class CalendarSocialApp(Flask): | |||||||
|             abort(404) |             abort(404) | ||||||
|  |  | ||||||
|         if profile.user != current_user: |         if profile.user != current_user: | ||||||
|             follow = UserFollow(follower=current_user.profile, |             profile.follow(follower=current_user.profile) | ||||||
|                                 followed=profile, |  | ||||||
|                                 accepted_at=datetime.utcnow()) |  | ||||||
|             db.session.add(follow) |  | ||||||
|  |  | ||||||
|             notification = Notification(profile=profile, |  | ||||||
|                                         actor=current_user.profile, |  | ||||||
|                                         item=profile, |  | ||||||
|                                         action=NotificationAction.follow) |  | ||||||
|             db.session.add(notification) |  | ||||||
|  |  | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         return redirect(url_for('display_profile', username=username)) |         return redirect(url_for('display_profile', username=username)) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @route('/notifications') |     @RoutedMixin.route('/accept/<int:invite_id>') | ||||||
|     def notifications(): |  | ||||||
|         """View to list the notifications for the current user |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         from .models import Notification |  | ||||||
|  |  | ||||||
|         if current_user.is_authenticated: |  | ||||||
|             notifs = Notification.query.filter(Notification.profile == current_user.profile) |  | ||||||
|         else: |  | ||||||
|             notifs = [] |  | ||||||
|  |  | ||||||
|         return render_template('notifications.html', notifs=notifs) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     @route('/accept/<int:invite_id>') |  | ||||||
|     def accept_invite(invite_id): |     def accept_invite(invite_id): | ||||||
|         """View to accept an invitation |         """View to accept an invitation | ||||||
|         """ |         """ | ||||||
| @@ -373,31 +346,21 @@ class CalendarSocialApp(Flask): | |||||||
|         return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) |         return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid)) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     @route('/first-steps', methods=['GET', 'POST']) |     @RoutedMixin.route('/all-events') | ||||||
|     @login_required |     def all_events(): | ||||||
|     def first_steps(): |         """View for listing all available events | ||||||
|         """View to set up a new registrant’s profile |  | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from .forms import FirstStepsForm |         from .calendar_system.gregorian import GregorianCalendar | ||||||
|         from .models import db, Profile |  | ||||||
|  |  | ||||||
|         if current_user.profile: |         try: | ||||||
|             return redirect(url_for('hello')) |             timestamp = datetime.fromtimestamp(float(request.args.get('date'))) | ||||||
|  |         except TypeError: | ||||||
|  |             timestamp = datetime.utcnow() | ||||||
|  |  | ||||||
|         form = FirstStepsForm() |         calendar = GregorianCalendar(timestamp.timestamp()) | ||||||
|  |  | ||||||
|         if form.validate_on_submit(): |         return render_template('index.html', calendar=calendar, user_only=False) | ||||||
|             profile = Profile(user=current_user, display_name=form.display_name.data) |  | ||||||
|             db.session.add(profile) |  | ||||||
|  |  | ||||||
|             current_user.settings['timezone'] = str(form.time_zone.data) |  | ||||||
|  |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|             return redirect(url_for('hello')) |  | ||||||
|  |  | ||||||
|         return render_template('first-steps.html', form=form) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = CalendarSocialApp(__name__) | app = CalendarSocialApp(__name__) | ||||||
|   | |||||||
							
								
								
									
										234
									
								
								calsocial/account.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								calsocial/account.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | |||||||
|  | # Calendar.social | ||||||
|  | # Copyright (C) 2018 Gergely Polonkai | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU Affero General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU Affero General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU Affero General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | """Main module for the Calendar.social app | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from flask import Blueprint, abort, current_app, flash, redirect, render_template, session, url_for | ||||||
|  | from flask_babelex import gettext as _ | ||||||
|  | from flask_security import current_user, login_required | ||||||
|  |  | ||||||
|  | from sqlalchemy.orm.exc import NoResultFound | ||||||
|  |  | ||||||
|  | from calsocial.utils import RoutedMixin | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AccountBlueprint(Blueprint, RoutedMixin): | ||||||
|  |     """Blueprint for account management | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         Blueprint.__init__(self, 'account', __name__) | ||||||
|  |  | ||||||
|  |         self.app = None | ||||||
|  |  | ||||||
|  |         RoutedMixin.register_routes(self) | ||||||
|  |  | ||||||
|  |     def init_app(self, app, url_prefix=None): | ||||||
|  |         """Initialise the blueprint, registering it with ``app``. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.app = app | ||||||
|  |  | ||||||
|  |         app.register_blueprint(self, url_prefix=url_prefix) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/register', 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 registrant’s profile | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .forms import FirstStepsForm | ||||||
|  |         from .models import db, Profile | ||||||
|  |  | ||||||
|  |         if current_user.profile: | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         form = FirstStepsForm() | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             profile = Profile(user=current_user, display_name=form.display_name.data) | ||||||
|  |             db.session.add(profile) | ||||||
|  |  | ||||||
|  |             current_user.settings['timezone'] = str(form.time_zone.data) | ||||||
|  |  | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('hello')) | ||||||
|  |  | ||||||
|  |         return render_template('account/first-steps.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/edit-profile', methods=['GET', 'POST']) | ||||||
|  |     @login_required | ||||||
|  |     def edit_profile(): | ||||||
|  |         """View for editing one’s profile | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .forms import ProfileForm | ||||||
|  |         from .models import db | ||||||
|  |  | ||||||
|  |         form = ProfileForm(current_user.profile) | ||||||
|  |  | ||||||
|  |         if form.validate_on_submit(): | ||||||
|  |             form.populate_obj(current_user.profile) | ||||||
|  |             db.session.add(current_user.profile) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |             return redirect(url_for('account.edit_profile')) | ||||||
|  |  | ||||||
|  |         return render_template('account/profile-edit.html', form=form) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/follow-requests') | ||||||
|  |     @login_required | ||||||
|  |     def follow_requests(): | ||||||
|  |         """View for listing follow requests | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .models import UserFollow | ||||||
|  |  | ||||||
|  |         requests = UserFollow.query \ | ||||||
|  |                              .filter(UserFollow.followed == current_user.profile) \ | ||||||
|  |                              .filter(UserFollow.accepted_at.is_(None)) | ||||||
|  |  | ||||||
|  |         return render_template('account/follow-requests.html', requests=requests) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/follow-request/<int:follower_id>/accept') | ||||||
|  |     @login_required | ||||||
|  |     def accept_follow(follower_id): | ||||||
|  |         """View for accepting a follow request | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         from .models import db, UserFollow | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             req = UserFollow.query \ | ||||||
|  |                             .filter(UserFollow.followed == current_user.profile) \ | ||||||
|  |                             .filter(UserFollow.follower_id == follower_id) \ | ||||||
|  |                             .one() | ||||||
|  |         except NoResultFound: | ||||||
|  |             abort(404) | ||||||
|  |  | ||||||
|  |         if req.accepted_at is None: | ||||||
|  |             req.accept() | ||||||
|  |  | ||||||
|  |             db.session.add(req) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |         return redirect(url_for('account.follow_requests')) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/sessions') | ||||||
|  |     @login_required | ||||||
|  |     def active_sessions(): | ||||||
|  |         """View the list of active sessions | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         sessions = [current_app.session_interface.load_session(sid) | ||||||
|  |                     for sid in current_user.active_sessions] | ||||||
|  |  | ||||||
|  |         return render_template('account/active-sessions.html', sessions=sessions) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     @RoutedMixin.route('/sessions/invalidate/<string:sid>') | ||||||
|  |     @login_required | ||||||
|  |     def invalidate_session(sid): | ||||||
|  |         """View to invalidate a session | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         sess = current_app.session_interface.load_session(sid) | ||||||
|  |  | ||||||
|  |         if not sess or sess.user != current_user: | ||||||
|  |             abort(404) | ||||||
|  |  | ||||||
|  |         if sess.sid == session.sid: | ||||||
|  |             flash(_('Can’t invalidate your current session')) | ||||||
|  |         else: | ||||||
|  |             current_app.session_interface.delete_session(sid) | ||||||
|  |             current_user.active_sessions = [sess_id | ||||||
|  |                                             for sess_id in current_user.active_sessions | ||||||
|  |                                             if sess_id != sid] | ||||||
|  |  | ||||||
|  |         return redirect(url_for('account.active_sessions')) | ||||||
							
								
								
									
										60
									
								
								calsocial/app_state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								calsocial/app_state.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										159
									
								
								calsocial/cache.py
									
									
									
									
									
										Normal 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) | ||||||
| @@ -18,24 +18,12 @@ | |||||||
| """ | """ | ||||||
|  |  | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from functools import wraps |  | ||||||
|  |  | ||||||
| from flask_babelex import lazy_gettext as _ | from flask_babelex import lazy_gettext as _ | ||||||
|  |  | ||||||
| from . import CalendarSystem | from . import CalendarSystem | ||||||
|  |  | ||||||
|  |  | ||||||
| def to_timestamp(func): |  | ||||||
|     """Decorator that converts the return value of a function from `datetime` to a UNIX timestamp |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     @wraps(func) |  | ||||||
|     def _decorator(*args, **kwargs): |  | ||||||
|         return func(*args, **kwargs).timestamp() |  | ||||||
|  |  | ||||||
|     return _decorator |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GregorianCalendar(CalendarSystem): | class GregorianCalendar(CalendarSystem): | ||||||
|     """Gregorian calendar system for Calendar.social |     """Gregorian calendar system for Calendar.social | ||||||
|     """ |     """ | ||||||
| @@ -83,22 +71,27 @@ class GregorianCalendar(CalendarSystem): | |||||||
|     def days(self): |     def days(self): | ||||||
|         day_list = [] |         day_list = [] | ||||||
|  |  | ||||||
|         start_day = self.timestamp.replace(day=1) |         month_first = self.timestamp.replace(day=1) | ||||||
|  |  | ||||||
|         while start_day.weekday() > self.START_DAY: |         if self.timestamp.month == 12: | ||||||
|             start_day -= timedelta(days=1) |             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) |         pad_before = (7 - self.START_DAY + month_first.weekday()) % 7 | ||||||
|         current_day = start_day |         pad_after = (6 - month_last.weekday() + self.START_DAY) % 7 | ||||||
|  |  | ||||||
|         while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month: |         first_display = month_first - timedelta(days=pad_before) | ||||||
|             current_day += timedelta(days=1) |         last_display = month_last + timedelta(days=pad_after) | ||||||
|             day_list.append(current_day) |         current = first_display | ||||||
|  |  | ||||||
|  |         while current <= last_display: | ||||||
|  |             day_list.append(current) | ||||||
|  |             current += timedelta(days=1) | ||||||
|  |  | ||||||
|         return day_list |         return day_list | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     @to_timestamp |  | ||||||
|     def prev_year(self): |     def prev_year(self): | ||||||
|         """Returns the timestamp of the same date in the previous year |         """Returns the timestamp of the same date in the previous year | ||||||
|         """ |         """ | ||||||
| @@ -113,7 +106,6 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         return self.timestamp.replace(year=self.timestamp.year - 1).year |         return self.timestamp.replace(year=self.timestamp.year - 1).year | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     @to_timestamp |  | ||||||
|     def prev_month(self): |     def prev_month(self): | ||||||
|         """Returns the timestamp of the same day in the previous month |         """Returns the timestamp of the same day in the previous month | ||||||
|         """ |         """ | ||||||
| @@ -136,7 +128,6 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         return self.month_names[timestamp.month - 1] |         return self.month_names[timestamp.month - 1] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     @to_timestamp |  | ||||||
|     def next_month(self): |     def next_month(self): | ||||||
|         """Returns the timestamp of the same day in the next month |         """Returns the timestamp of the same day in the next month | ||||||
|         """ |         """ | ||||||
| @@ -159,7 +150,6 @@ class GregorianCalendar(CalendarSystem): | |||||||
|         return self.month_names[timestamp.month - 1] |         return self.month_names[timestamp.month - 1] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     @to_timestamp |  | ||||||
|     def next_year(self): |     def next_year(self): | ||||||
|         """Returns the timestamp of the same date in the next year |         """Returns the timestamp of the same date in the next year | ||||||
|         """ |         """ | ||||||
| @@ -192,28 +182,36 @@ class GregorianCalendar(CalendarSystem): | |||||||
|  |  | ||||||
|         month_end_timestamp = month_start_timestamp.replace(month=next_month) |         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 |     @staticmethod | ||||||
|     def day_events(date, user=None): |     def day_events(date, user=None): | ||||||
|         """Returns all events for a given day |         """Returns all events for a given day | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from ..models import Event, Profile |         from ..models import Event, EventVisibility, Invitation, Profile, Response | ||||||
|  |  | ||||||
|         events = Event.query |         events = Event.query | ||||||
|  |  | ||||||
|         if user: |         if user: | ||||||
|             events = events.join(Profile) \ |             events = events.outerjoin(Invitation) \ | ||||||
|  |                            .outerjoin(Response) \ | ||||||
|  |                            .join(Profile, Event.profile) \ | ||||||
|                            .filter(Profile.user == user) |                            .filter(Profile.user == user) | ||||||
|  |  | ||||||
|         start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) |         start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) | ||||||
|         end_timestamp = start_timestamp + timedelta(days=1) |         end_timestamp = start_timestamp + timedelta(days=1) | ||||||
|  |  | ||||||
|         events = events.filter(((Event.start_time >= start_timestamp) & |         events = events.filter((Event.start_time <= end_timestamp) & | ||||||
|                                 (Event.start_time < end_timestamp)) | |                                (Event.end_time >= start_timestamp)) \ | ||||||
|                                ((Event.end_time >= start_timestamp) & |  | ||||||
|                                 (Event.end_time < end_timestamp))) \ |  | ||||||
|                        .order_by('start_time', 'end_time') |                        .order_by('start_time', 'end_time') | ||||||
|  |  | ||||||
|  |         if user is None: | ||||||
|  |             events = events.filter(Event.visibility == EventVisibility.public) | ||||||
|  |         else: | ||||||
|  |             events = events.filter((Event.visibility == EventVisibility.public) | | ||||||
|  |                                    (Event.profile == user.profile) | | ||||||
|  |                                    (Invitation.invitee == user.profile) | | ||||||
|  |                                    (Response.profile == user.profile)) | ||||||
|  |  | ||||||
|         return events |         return events | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Configuration file for the development environment | """Configuration file for the development environment | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| ENV = 'dev' | ENV = 'development' | ||||||
| #: If ``True``, registration on the site is enabled. | #: If ``True``, registration on the site is enabled. | ||||||
| REGISTRATION_ENABLED = True | REGISTRATION_ENABLED = True | ||||||
| #: The default time zone | #: The default time zone | ||||||
| @@ -14,3 +14,4 @@ SECRET_KEY = 'ThisIsNotSoSecret' | |||||||
| SECURITY_PASSWORD_HASH = 'bcrypt' | SECURITY_PASSWORD_HASH = 'bcrypt' | ||||||
| SECURITY_PASSWORD_SALT = SECRET_KEY | SECURITY_PASSWORD_SALT = SECRET_KEY | ||||||
| SECURITY_REGISTERABLE = False | SECURITY_REGISTERABLE = False | ||||||
|  | CACHE_TYPE = 'simple' | ||||||
							
								
								
									
										18
									
								
								calsocial/config_testing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								calsocial/config_testing.py
									
									
									
									
									
										Normal 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' | ||||||
| @@ -17,17 +17,21 @@ | |||||||
| """Forms for Calendar.social | """Forms for Calendar.social | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
| from flask_babelex import lazy_gettext as _ | from flask_babelex import lazy_gettext as _ | ||||||
| from flask_security.forms import LoginForm as BaseLoginForm | from flask_security.forms import LoginForm as BaseLoginForm | ||||||
| from flask_wtf import FlaskForm | from flask_wtf import FlaskForm | ||||||
| import pytz | import pytz | ||||||
| from wtforms import BooleanField, PasswordField, SelectField, StringField | from wtforms import BooleanField, PasswordField, SelectField, StringField, RadioField | ||||||
| from wtforms.ext.dateutil.fields import DateTimeField | from wtforms.ext.dateutil.fields import DateTimeField | ||||||
| from wtforms.validators import DataRequired, Email, StopValidation, ValidationError | from wtforms.validators import DataRequired, Email, StopValidation, ValidationError | ||||||
| from wtforms.widgets import TextArea | from wtforms.widgets import TextArea | ||||||
|  |  | ||||||
|  | from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS | ||||||
|  |  | ||||||
| class UsernameAvailable(object):  # pylint: disable=too-few-public-methods |  | ||||||
|  | class UsernameAvailable:  # pylint: disable=too-few-public-methods | ||||||
|     """Checks if a username is available |     """Checks if a username is available | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
| @@ -58,7 +62,7 @@ class UsernameAvailable(object):  # pylint: disable=too-few-public-methods | |||||||
|         raise StopValidation(message) |         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 |     """Checks if an email address is available | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
| @@ -169,6 +173,45 @@ class TimezoneField(SelectField): | |||||||
|             yield (value, label, value == self.data) |             yield (value, label, value == self.data) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EnumField(SelectField): | ||||||
|  |     """Field that allows selecting one value from an ``Enum`` class | ||||||
|  |  | ||||||
|  |     :param enum_type: an ``Enum`` type | ||||||
|  |     :type enum_type: type(Enum) | ||||||
|  |     :param translations: translatable labels for enum values | ||||||
|  |     :type translations: dict | ||||||
|  |     :param args: passed verbatim to the constructor of `SelectField` | ||||||
|  |     :param kwargs: passed verbatim to the constructor of `SelectField` | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, enum_type, translations, *args, **kwargs): | ||||||
|  |         if not issubclass(enum_type, Enum): | ||||||
|  |             raise TypeError('enum_type must be a subclass of Enum') | ||||||
|  |  | ||||||
|  |         kwargs.update({'choices': [(value, None) for value in enum_type]}) | ||||||
|  |         self.data = None | ||||||
|  |         self.enum_type = enum_type | ||||||
|  |         self.translations = translations | ||||||
|  |         SelectField.__init__(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def process_formdata(self, valuelist): | ||||||
|  |         if not valuelist: | ||||||
|  |             self.data = None | ||||||
|  |  | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             self.data = self.enum_type[valuelist[0]] | ||||||
|  |         except KeyError: | ||||||
|  |             raise ValueError('Unknown value') | ||||||
|  |  | ||||||
|  |     def iter_choices(self): | ||||||
|  |         for value in self.enum_type: | ||||||
|  |             label = self.gettext(self.translations[value]) if self.translations else value.name | ||||||
|  |  | ||||||
|  |             yield (value.name, label, value == self.data) | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventForm(FlaskForm): | class EventForm(FlaskForm): | ||||||
|     """Form for event creation/editing |     """Form for event creation/editing | ||||||
|     """ |     """ | ||||||
| @@ -179,6 +222,14 @@ class EventForm(FlaskForm): | |||||||
|     end_time = DateTimeField(_('End time'), validators=[DataRequired()]) |     end_time = DateTimeField(_('End time'), validators=[DataRequired()]) | ||||||
|     all_day = BooleanField(_('All day')) |     all_day = BooleanField(_('All day')) | ||||||
|     description = StringField(_('Description'), widget=TextArea()) |     description = StringField(_('Description'), widget=TextArea()) | ||||||
|  |     visibility = EnumField(EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, label=_('Visibility')) | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         from flask_security import current_user | ||||||
|  |  | ||||||
|  |         self.time_zone.kwargs['default'] = current_user.timezone  # pylint: disable=no-member | ||||||
|  |  | ||||||
|  |         FlaskForm.__init__(self, *args, **kwargs) | ||||||
|  |  | ||||||
|     def populate_obj(self, obj): |     def populate_obj(self, obj): | ||||||
|         """Populate ``obj`` with event data |         """Populate ``obj`` with event data | ||||||
| @@ -330,8 +381,33 @@ class FirstStepsForm(FlaskForm): | |||||||
|     display_name = StringField( |     display_name = StringField( | ||||||
|         label=_('Display name'), |         label=_('Display name'), | ||||||
|         validators=[DataRequired()], |         validators=[DataRequired()], | ||||||
|  |         # pylint: disable=line-too-long | ||||||
|         description=_('This will be shown to other users as your name.  You can use your real name, or any nickname you like.')) |         description=_('This will be shown to other users as your name.  You can use your real name, or any nickname you like.')) | ||||||
|     time_zone = TimezoneField( |     time_zone = TimezoneField( | ||||||
|         label=_('Your time zone'), |         label=_('Your time zone'), | ||||||
|         validators=[DataRequired()], |         validators=[DataRequired()], | ||||||
|         description=_('The start and end times of events will be displayed in this time zone.')) |         description=_('The start and end times of events will be displayed in this time zone.')) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ProfileForm(FlaskForm): | ||||||
|  |     """Form for editing a user profile | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     display_name = StringField(label=_('Display name'), validators=[DataRequired()]) | ||||||
|  |     builtin_avatar = RadioField(label=_('Use a built-in avatar')) | ||||||
|  |     locked = BooleanField(label=_('Lock profile')) | ||||||
|  |  | ||||||
|  |     def __init__(self, profile, *args, **kwargs): | ||||||
|  |         from flask import current_app | ||||||
|  |  | ||||||
|  |         kwargs.update( | ||||||
|  |             { | ||||||
|  |                 'display_name': profile.display_name, | ||||||
|  |                 'locked': profile.locked, | ||||||
|  |                 'builtin_avatar': profile.builtin_avatar, | ||||||
|  |             }) | ||||||
|  |         FlaskForm.__init__(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         self.builtin_avatar.choices = [(name, name) | ||||||
|  |                                        for name in current_app.config['BUILTIN_AVATARS']] | ||||||
|  |         self.profile = profile | ||||||
|   | |||||||
| @@ -21,12 +21,15 @@ from datetime import datetime | |||||||
| from enum import Enum | from enum import Enum | ||||||
| from warnings import warn | from warnings import warn | ||||||
|  |  | ||||||
|  | from flask import current_app | ||||||
| from flask_babelex import lazy_gettext | from flask_babelex import lazy_gettext | ||||||
| from flask_security import UserMixin, RoleMixin | from flask_security import UserMixin, RoleMixin | ||||||
| from flask_sqlalchemy import SQLAlchemy | from flask_sqlalchemy import SQLAlchemy | ||||||
| from sqlalchemy.orm.exc import NoResultFound | from sqlalchemy.orm.exc import NoResultFound | ||||||
| from sqlalchemy_utils.types.choice import ChoiceType | from sqlalchemy_utils.types.choice import ChoiceType | ||||||
|  |  | ||||||
|  | from .app_state import app_state_base | ||||||
|  | from .cache import cache | ||||||
| from .utils import force_locale | from .utils import force_locale | ||||||
|  |  | ||||||
| db = SQLAlchemy() | db = SQLAlchemy() | ||||||
| @@ -98,11 +101,28 @@ class ResponseType(Enum): | |||||||
|             return self.name.lower() == other.lower()  # pylint: disable=no-member |             return self.name.lower() == other.lower()  # pylint: disable=no-member | ||||||
|  |  | ||||||
|         if isinstance(other, (int, float)): |         if isinstance(other, (int, float)): | ||||||
|             return self.value == other |             return self.value == other  # pylint: disable=comparison-with-callable | ||||||
|  |  | ||||||
|         return Enum.__eq__(self, other) |         return Enum.__eq__(self, other) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventVisibility(Enum): | ||||||
|  |     """Enumeration for event visibility | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     #: The event is private, only attendees and people invited can see the details | ||||||
|  |     private = 0 | ||||||
|  |  | ||||||
|  |     #: The event is public, anyone can see the details | ||||||
|  |     public = 5 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | EVENT_VISIBILITY_TRANSLATIONS = { | ||||||
|  |     EventVisibility.private: _('Visible only to attendees'), | ||||||
|  |     EventVisibility.public: _('Visible to everyone'), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class SettingsProxy: | class SettingsProxy: | ||||||
|     """Proxy object to get settings for a user |     """Proxy object to get settings for a user | ||||||
|     """ |     """ | ||||||
| @@ -188,7 +208,6 @@ class User(db.Model, UserMixin): | |||||||
|         If the user didn’t set a time zone yet, the application default is used. |         If the user didn’t set a time zone yet, the application default is used. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         from flask import current_app |  | ||||||
|         from pytz import timezone |         from pytz import timezone | ||||||
|         from pytz.exceptions import UnknownTimeZoneError |         from pytz.exceptions import UnknownTimeZoneError | ||||||
|  |  | ||||||
| @@ -202,6 +221,24 @@ class User(db.Model, UserMixin): | |||||||
|  |  | ||||||
|         return current_app.timezone |         return current_app.timezone | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def session_list_key(self): | ||||||
|  |         """The cache key of this user’s session list | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return f'open_sessions:{self.id}' | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def active_sessions(self): | ||||||
|  |         """The list of active sessions of this user | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return cache.get(self.session_list_key) or [] | ||||||
|  |  | ||||||
|  |     @active_sessions.setter | ||||||
|  |     def active_sessions(self, value): | ||||||
|  |         cache.set(self.session_list_key, list(value)) | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return f'<User {self.id}({self.username})>' |         return f'<User {self.id}({self.username})>' | ||||||
|  |  | ||||||
| @@ -246,6 +283,12 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|     #: The display name |     #: The display name | ||||||
|     display_name = db.Column(db.Unicode(length=80), nullable=False) |     display_name = db.Column(db.Unicode(length=80), nullable=False) | ||||||
|  |  | ||||||
|  |     #: If locked, a profile cannot be followed without the owner’s consent | ||||||
|  |     locked = db.Column(db.Boolean(), default=False) | ||||||
|  |  | ||||||
|  |     #: If set, the profile will display this builtin avatar | ||||||
|  |     builtin_avatar = db.Column(db.String(length=40), nullable=True) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def fqn(self): |     def fqn(self): | ||||||
|         """The fully qualified name of the profile |         """The fully qualified name of the profile | ||||||
| @@ -265,7 +308,7 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|             domain = '' |             domain = '' | ||||||
|         else: |         else: | ||||||
|             username = self.username |             username = self.username | ||||||
|             domain = '@' + self.domain |             domain = f'@{self.domain}' | ||||||
|  |  | ||||||
|         return f'<Profile {self.id}(@{username}{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 \ |         return Profile.query \ | ||||||
|             .join(UserFollow.followed) \ |             .join(UserFollow.followed) \ | ||||||
|             .filter(UserFollow.follower == self) |             .filter(UserFollow.follower == self) \ | ||||||
|  |             .filter(UserFollow.accepted_at.isnot(None)) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def follower_list(self): |     def follower_list(self): | ||||||
| @@ -303,7 +347,8 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
|         return Profile.query \ |         return Profile.query \ | ||||||
|             .join(UserFollow.follower) \ |             .join(UserFollow.follower) \ | ||||||
|             .filter(UserFollow.followed == self) |             .filter(UserFollow.followed == self) \ | ||||||
|  |             .filter(UserFollow.accepted_at.isnot(None)) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def url(self): |     def url(self): | ||||||
| @@ -317,6 +362,48 @@ class Profile(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
|         return NotImplemented |         return NotImplemented | ||||||
|  |  | ||||||
|  |     def follow(self, follower): | ||||||
|  |         """Make ``follower`` follow this profile | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not isinstance(follower, Profile): | ||||||
|  |             raise TypeError('Folloer must be a Profile object') | ||||||
|  |  | ||||||
|  |         timestamp = None if self.locked else datetime.utcnow() | ||||||
|  |  | ||||||
|  |         user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp) | ||||||
|  |         db.session.add(user_follow) | ||||||
|  |         notification = self.notify(follower, self, NotificationAction.follow) | ||||||
|  |  | ||||||
|  |         db.session.add(notification) | ||||||
|  |  | ||||||
|  |         return user_follow | ||||||
|  |  | ||||||
|  |     def notify(self, actor, item, action): | ||||||
|  |         """Notify this profile about ``action`` on ``item`` by ``actor`` | ||||||
|  |  | ||||||
|  |         :param actor: the actor who generated the notification | ||||||
|  |         :type actor: Profile | ||||||
|  |         :param item: the item ``action`` was performed on | ||||||
|  |         :type item: any | ||||||
|  |         :param action: the type of the action | ||||||
|  |         :type action: NotificationAction, str | ||||||
|  |         :raises TypeError: if ``actor`` is not a `Profile` object | ||||||
|  |         :returns: the generated notification.  It is already added to the database session, but | ||||||
|  |             not committed | ||||||
|  |         :rtype: Notification | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not isinstance(actor, Profile): | ||||||
|  |             raise TypeError('actor must be a Profile instance') | ||||||
|  |  | ||||||
|  |         if isinstance(action, str): | ||||||
|  |             action = NotificationAction[action] | ||||||
|  |  | ||||||
|  |         notification = Notification(profile=self, actor=actor, item=item, action=action) | ||||||
|  |  | ||||||
|  |         return notification | ||||||
|  |  | ||||||
|  |  | ||||||
| class Event(db.Model): | class Event(db.Model): | ||||||
|     """Database model for events |     """Database model for events | ||||||
| @@ -352,6 +439,9 @@ class Event(db.Model): | |||||||
|     #: The description of the event |     #: The description of the event | ||||||
|     description = db.Column(db.UnicodeText()) |     description = db.Column(db.UnicodeText()) | ||||||
|  |  | ||||||
|  |     #: The visibility of the event | ||||||
|  |     visibility = db.Column(db.Enum(EventVisibility), nullable=False) | ||||||
|  |  | ||||||
|     def __as_tz(self, timestamp, as_timezone=None): |     def __as_tz(self, timestamp, as_timezone=None): | ||||||
|         from pytz import timezone, utc |         from pytz import timezone, utc | ||||||
|  |  | ||||||
| @@ -400,6 +490,20 @@ class Event(db.Model): | |||||||
|  |  | ||||||
|         return url_for('event_details', event_uuid=self.event_uuid) |         return url_for('event_details', event_uuid=self.event_uuid) | ||||||
|  |  | ||||||
|  |     def invite(self, inviter, invited): | ||||||
|  |         """Invite ``invited`` to the event | ||||||
|  |  | ||||||
|  |         The invitation will arrive from ``inviter``. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         invite = Invitation(event=self, sender=inviter, invitee=invited) | ||||||
|  |         db.session.add(invite) | ||||||
|  |  | ||||||
|  |         notification = invited.notify(inviter, self, NotificationAction.invite) | ||||||
|  |         db.session.add(notification) | ||||||
|  |  | ||||||
|  |         return invite | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserSetting(db.Model):  # pylint: disable=too-few-public-methods | class UserSetting(db.Model):  # pylint: disable=too-few-public-methods | ||||||
|     """Database model for user settings |     """Database model for user settings | ||||||
| @@ -543,6 +647,12 @@ class UserFollow(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|     #: The timestamp when the follow was accepted |     #: The timestamp when the follow was accepted | ||||||
|     accepted_at = db.Column(db.DateTime(), nullable=True) |     accepted_at = db.Column(db.DateTime(), nullable=True) | ||||||
|  |  | ||||||
|  |     def accept(self): | ||||||
|  |         """Accept this follow request | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.accepted_at = datetime.utcnow() | ||||||
|  |  | ||||||
|  |  | ||||||
| class Notification(db.Model): | class Notification(db.Model): | ||||||
|     """Database model for notifications |     """Database model for notifications | ||||||
| @@ -680,3 +790,64 @@ class Response(db.Model):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
|     #: The response itself |     #: The response itself | ||||||
|     response = db.Column(db.Enum(ResponseType), nullable=False) |     response = db.Column(db.Enum(ResponseType), nullable=False) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AppState(app_state_base(db.Model)):  # pylint: disable=too-few-public-methods,inherit-non-class | ||||||
|  |     """Database model for application state values | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     __tablename__ = 'app_state' | ||||||
|  |  | ||||||
|  |     #: The environment that set this key | ||||||
|  |     env = db.Column(db.String(length=40), nullable=False, primary_key=True) | ||||||
|  |  | ||||||
|  |     #: The key | ||||||
|  |     key = db.Column(db.String(length=80), nullable=False, primary_key=True) | ||||||
|  |  | ||||||
|  |     #: The value of the key | ||||||
|  |     value = db.Column(db.Unicode(length=200), nullable=True) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def __get_state__(cls, key): | ||||||
|  |         try: | ||||||
|  |             record = cls.query \ | ||||||
|  |                         .filter(cls.env == current_app.env) \ | ||||||
|  |                         .filter(cls.key == key) \ | ||||||
|  |                         .one() | ||||||
|  |         except NoResultFound: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         return record.value | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def __set_state__(cls, key, value): | ||||||
|  |         try: | ||||||
|  |             record = cls.query \ | ||||||
|  |                         .filter(cls.env == current_app.env) \ | ||||||
|  |                         .filter(cls.key == key) \ | ||||||
|  |                         .one() | ||||||
|  |         except NoResultFound: | ||||||
|  |             record = cls(env=current_app.env, key=key) | ||||||
|  |  | ||||||
|  |         record.value = value | ||||||
|  |         db.session.add(record) | ||||||
|  |         db.session.commit() | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def __set_state_default__(cls, key, value): | ||||||
|  |         try: | ||||||
|  |             record = cls.query \ | ||||||
|  |                         .filter(cls.env == current_app.env) \ | ||||||
|  |                         .filter(cls.key == key) \ | ||||||
|  |                         .one() | ||||||
|  |         except NoResultFound: | ||||||
|  |             pass | ||||||
|  |         else: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         record = cls(env=current_app.env, key=key, value=value) | ||||||
|  |         db.session.add(record) | ||||||
|  |         db.session.commit() | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         return f'<AppState {self.env}:{self.key}="{self.value}"' | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
| """Security related things for Calendar.social | """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_login.signals import user_logged_in, user_logged_out | ||||||
| from flask_security import Security, AnonymousUser as BaseAnonymousUser | from flask_security import Security, AnonymousUser as BaseAnonymousUser | ||||||
|  |  | ||||||
| @@ -35,6 +35,15 @@ class AnonymousUser(BaseAnonymousUser): | |||||||
|  |  | ||||||
|         return current_app.timezone |         return current_app.timezone | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def profile(self): | ||||||
|  |         """The profile of the anonymous user | ||||||
|  |  | ||||||
|  |         Always evaluates to ``None`` | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
| @user_logged_in.connect | @user_logged_in.connect | ||||||
| def login_handler(app, user):  # pylint: disable=unused-argument | def login_handler(app, user):  # pylint: disable=unused-argument | ||||||
| @@ -45,6 +54,8 @@ def login_handler(app, user):  # pylint: disable=unused-argument | |||||||
|  |  | ||||||
|     AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) |     AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) | ||||||
|  |  | ||||||
|  |     user.active_sessions += [session.sid] | ||||||
|  |  | ||||||
|  |  | ||||||
| @user_logged_out.connect | @user_logged_out.connect | ||||||
| def logout_handler(app, user):  # pylint: disable=unused-argument | def logout_handler(app, user):  # pylint: disable=unused-argument | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								calsocial/static/avatars/doctor.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								calsocial/static/avatars/doctor.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										54
									
								
								calsocial/static/avatars/engineer.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								calsocial/static/avatars/engineer.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										91
									
								
								calsocial/static/avatars/scientist.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								calsocial/static/avatars/scientist.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										39
									
								
								calsocial/static/avatars/statistician.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								calsocial/static/avatars/statistician.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										11
									
								
								calsocial/static/avatars/user.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								calsocial/static/avatars/user.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										26
									
								
								calsocial/static/avatars/whoami.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								calsocial/static/avatars/whoami.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										121
									
								
								calsocial/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								calsocial/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | header > h1 > img { | ||||||
|  |     height: 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #content { | ||||||
|  |     margin-top: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ui.profile { | ||||||
|  |     display: block; | ||||||
|  |     position: relative; | ||||||
|  |     height: 50px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ui.profile > .avatar { | ||||||
|  |     width: 48px; | ||||||
|  |     height: 48px; | ||||||
|  |     border: 1px solid black; | ||||||
|  |     border-radius: 50%; | ||||||
|  |     position: absolute; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ui.profile > .display.name { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 58px; | ||||||
|  |     font-weight: bold; | ||||||
|  |     color: #000000; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ui.profile > .handle { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 1.5em; | ||||||
|  |     left: 58px; | ||||||
|  |     color: #666666; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ui.centered.statistics { | ||||||
|  |     justify-content: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .timezone-warning { | ||||||
|  |     color: #e94a4a; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | footer { | ||||||
|  |     margin-top: 3em; | ||||||
|  |     font-weight: bold; | ||||||
|  |     border-top: 1px dotted black; | ||||||
|  |     padding-top: 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media not speech { | ||||||
|  |     .sr-only { | ||||||
|  |         display: none; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | table.calendar > * { | ||||||
|  |     font-family: sans; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | table.calendar { | ||||||
|  |     width: 100%; | ||||||
|  |     border-collapse: collapse; | ||||||
|  |     table-layout: fixed; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.month > td { | ||||||
|  |     text-align: center; | ||||||
|  |     font-weight: bold; | ||||||
|  |     border-bottom: 2px solid black; | ||||||
|  |     padding-bottom: .5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.month > td > a { | ||||||
|  |     font-weight: normal; | ||||||
|  |     color: black; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.month > td.month-name > a { | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.days > td { | ||||||
|  |     text-align: center; | ||||||
|  |     border-bottom: 2px solid black; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.week > td { | ||||||
|  |     height: 3em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.week > td > span.day-num { | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.week > td.other-month > span.day-num { | ||||||
|  |     font-weight: normal; | ||||||
|  |     color: #909090; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.week > td.today { | ||||||
|  |     background-color: #d8d8d8; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.week > td > a.event { | ||||||
|  |     display: block; | ||||||
|  |     color: black; | ||||||
|  |     text-decoration: none; | ||||||
|  |     border: 1px solid green; | ||||||
|  |     background-color: white; | ||||||
|  |     border-radius: 2px; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr.sizer > td { | ||||||
|  |     width: 14.2857%; | ||||||
|  |     height: 0; | ||||||
|  | } | ||||||
							
								
								
									
										364
									
								
								calsocial/static/semantic/semantic.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								calsocial/static/semantic/semantic.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										11
									
								
								calsocial/static/semantic/semantic.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								calsocial/static/semantic/semantic.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										30
									
								
								calsocial/templates/_macros.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								calsocial/templates/_macros.html
									
									
									
									
									
										Normal 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 %} | ||||||
							
								
								
									
										15
									
								
								calsocial/templates/account/active-sessions.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								calsocial/templates/account/active-sessions.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | {% extends 'account/settings-base.html' %} | ||||||
|  |  | ||||||
|  | {% block settings_content %} | ||||||
|  | <h2>{% trans %}Active sessions{% endtrans %}</h2> | ||||||
|  | <ul> | ||||||
|  |     {% for sess in sessions %} | ||||||
|  |     <li> | ||||||
|  |         {{ sess['ip'] }} | ||||||
|  |         {% if sess.sid != session.sid %} | ||||||
|  |         <a href="{{ url_for('account.invalidate_session', sid=sess.sid) }}">{% trans %}Invalidate{% endtrans %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |     </li> | ||||||
|  |     {% endfor %} | ||||||
|  | </ul> | ||||||
|  | {% endblock settings_content %} | ||||||
							
								
								
									
										27
									
								
								calsocial/templates/account/first-steps.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								calsocial/templates/account/first-steps.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <h1>{% trans %}First steps{% endtrans %}</h1> | ||||||
|  | <p> | ||||||
|  |     {% trans %}Welcome to Calendar.social!{% endtrans %} | ||||||
|  | </p> | ||||||
|  | <p> | ||||||
|  |     {% trans %}These are the first steps you should make before you can start using the site.{% endtrans %} | ||||||
|  | </p> | ||||||
|  | <form method="post" class="ui form"> | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {% if form.errors %} | ||||||
|  |     {% for error in form.errors %} | ||||||
|  |     {{ error }} | ||||||
|  |     {% endfor %} | ||||||
|  |     <br> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     {{ field(form.display_name) }} | ||||||
|  |     {{ field(form.time_zone) }} | ||||||
|  |  | ||||||
|  |     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock content %} | ||||||
							
								
								
									
										23
									
								
								calsocial/templates/account/follow-requests.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								calsocial/templates/account/follow-requests.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <h2>{% trans %}Follow requests{% endtrans %}</h2> | ||||||
|  |     {% if requests.count() %} | ||||||
|  | <ul> | ||||||
|  |         {% for req in requests %} | ||||||
|  |     <li> | ||||||
|  |         {{ req.follower }} | ||||||
|  |         <a href="{{ url_for('account.accept_follow', follower_id=req.follower_id) }}" class="ui button">{% trans %}Accept{% endtrans %}</a> | ||||||
|  |     </li> | ||||||
|  |         {% endfor %} | ||||||
|  | </ul> | ||||||
|  |     {% else %} | ||||||
|  | {% trans %}No requests to display.{% endtrans %} | ||||||
|  |     {% endif %} | ||||||
|  |     {% if not current_user.profile.locked %} | ||||||
|  | <p> | ||||||
|  |     {% trans %}Your profile is not locked.{% endtrans %} | ||||||
|  |     {% trans %}Anyone can follow you without your consent.{% endtrans %} | ||||||
|  | </p> | ||||||
|  |     {% endif %} | ||||||
|  | {% endblock content %} | ||||||
| @@ -3,5 +3,7 @@ | |||||||
| {% block content %} | {% block content %} | ||||||
| {% for notif in notifs %} | {% for notif in notifs %} | ||||||
| {{ notif.html }}<br> | {{ notif.html }}<br> | ||||||
|  | {% else %} | ||||||
|  | {% trans %}Nothing to show.{% endtrans %} | ||||||
| {% endfor %} | {% endfor %} | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
							
								
								
									
										22
									
								
								calsocial/templates/account/profile-edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								calsocial/templates/account/profile-edit.html
									
									
									
									
									
										Normal 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 %} | ||||||
							
								
								
									
										19
									
								
								calsocial/templates/account/registration.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								calsocial/templates/account/registration.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <form method="post" class="ui form"> | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {% for error in form.errors %} | ||||||
|  |     {{ form.errors }} | ||||||
|  |     {% endfor %} | ||||||
|  |  | ||||||
|  |     {{ field(form.username) }} | ||||||
|  |     {{ field(form.email) }} | ||||||
|  |     {{ field(form.password) }} | ||||||
|  |     {{ field(form.password_retype) }} | ||||||
|  |  | ||||||
|  |     <button type="submit" class="ui primary button">{% trans %}Register{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										16
									
								
								calsocial/templates/account/settings-base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								calsocial/templates/account/settings-base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="ui grid"> | ||||||
|  |     <div class="four wide column"> | ||||||
|  |         <div class="ui secondary pointing vertical menu"> | ||||||
|  |             <a class="item{% if request.endpoint == 'account.edit_profile' %} active{% endif %}" href="{{ url_for('account.edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a> | ||||||
|  |             <a class="item{% if request.endpoint == 'account.settings' %} active{% endif %}" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a> | ||||||
|  |             <a class="item{% if request.endpoint == 'account.active_sessions' %} active{% endif %}" href="{{ url_for('account.active_sessions') }}">{% trans %}Active sessions{% endtrans %}</a> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="twelve wide stretched column"> | ||||||
|  |     {% block settings_content %}{% endblock %} | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										20
									
								
								calsocial/templates/account/user-settings.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								calsocial/templates/account/user-settings.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | {% extends 'account/settings-base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
|  | {% block settings_content %} | ||||||
|  | <h2>{% trans %}Settings{% endtrans %}</h2> | ||||||
|  | <form method="post" class="ui form"> | ||||||
|  |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {% if form.errors %} | ||||||
|  |     {% for error in form.errors %} | ||||||
|  |     {{ error }} | ||||||
|  |     {% endfor %} | ||||||
|  |     <br> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     {{ field(form.timezone) }} | ||||||
|  |  | ||||||
|  |     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock settings_content %} | ||||||
| @@ -7,47 +7,49 @@ | |||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-32.png') }}"> | ||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-96.png') }}"> | ||||||
|         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}"> |         <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}"> | ||||||
| {% block head %} |  | ||||||
|         <style> |  | ||||||
|          header > h1 > img { |  | ||||||
|              height: 1em; |  | ||||||
|          } |  | ||||||
|  |  | ||||||
|          footer { |         <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous"> | ||||||
|              margin-top: 3em; |         <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}"> | ||||||
|              font-weight: bold; |  | ||||||
|              border-top: 1px dotted black; |         <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css"> | ||||||
|              padding-top: 1em; |  | ||||||
|          } |         <meta charset="utf-8"> | ||||||
|         </style> |         <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||||||
| {% endblock %} | {% block head %}{% endblock %} | ||||||
|     </head> |     </head> | ||||||
|     <body> |     <body> | ||||||
|         <header> |         <header> | ||||||
|             <h1> |             <div class="ui top attached menu"> | ||||||
|  |                 <div class="header item"> | ||||||
|                     <img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}"> |                     <img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}"> | ||||||
|                     Calendar.social |                     Calendar.social | ||||||
|             </h1> |                 </div> | ||||||
|             <nav class="menu"> |                 <div class="right menu"> | ||||||
| {% if current_user.is_authenticated %} |  | ||||||
|                 {{ _('Logged in as %(username)s', username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe) }} |  | ||||||
| {% endif %} |  | ||||||
|                 <ul> |  | ||||||
| {% if not current_user.is_authenticated %} | {% 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 %} | {% else %} | ||||||
|                     <li><a href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a></li> |                     <div class="item"> | ||||||
|                     <li><a href="{{ url_for('notifications') }}">{% trans %}Notifications{% endtrans %}</a></li> |     {% trans username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe -%} | ||||||
|                     <li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li> |                         Logged in as {{username}} | ||||||
|                     <li><a href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a></li> |     {%- endtrans %} | ||||||
|  |                     </div> | ||||||
|  |                     <a class="item" href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a> | ||||||
|  |                     <a class="item" href="{{ url_for('account.notifications') }}">{% trans %}Notifications{% endtrans %}</a> | ||||||
|  |                     <a class="item" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a> | ||||||
|  |                     <a class="item" href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a> | ||||||
| {% endif %} | {% endif %} | ||||||
|                 </ul> |                 </div> | ||||||
|             </nav> |             </div> | ||||||
|         </header> |         </header> | ||||||
|  |         <div class="ui container" id="content"> | ||||||
| {% block content %}{% endblock %} | {% block content %}{% endblock %} | ||||||
|         <footer> |         </div> | ||||||
|  |         <footer class="ui segment"> | ||||||
|  |             <a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br> | ||||||
|             Soon…™ |             Soon…™ | ||||||
|         </footer> |         </footer> | ||||||
| {% block scripts %}{% endblock %} | {% block scripts %}{% endblock %} | ||||||
|  |         <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script> | ||||||
|  |         <script src="{{ url_for('static', filename='semantic/semantic.min.js') }}"></script> | ||||||
|     </body> |     </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -1,15 +1,29 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
|  | {% macro time_zone_warning() %} | ||||||
|  |     {% trans timezone=event.time_zone, start_time=event.start_time_tz | datetimeformat(rebase=false), end_time=event.end_time_tz | datetimeformat(rebase=false) -%} | ||||||
|  | This event is organised in the {{timezone}} time zone, in which it happens between {{start_time}} and {{end_time}} | ||||||
|  |     {%- endtrans %} | ||||||
|  | {% endmacro %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h1> | <h2 class="ui header"> | ||||||
|  |     <div class="content"> | ||||||
|         {{ event.title }}<br> |         {{ event.title }}<br> | ||||||
|     <small> |         <div class="sub header"> | ||||||
|         {{ event.start_time_for_user(current_user) }}–{{ event.end_time_for_user(current_user) }} |     {%- if current_user.timezone | string != event.time_zone -%} | ||||||
|     {% if current_user.timezone | string != event.time_zone %} |             <span title="{{ time_zone_warning() }}"> | ||||||
|         ({{ event.start_time_tz }}–{{ event.end_time_tz }} {{ event.time_zone }}) |                 <i class="fa fa-exclamation-triangle timezone-warning"></i> | ||||||
|  |                 <span class="sr-only">{{ time_zone_warning() }}</span> | ||||||
|  |             </span> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     </small> |             {{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }} | ||||||
| </h1> |             – | ||||||
|  |             {{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }} | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </h2> | ||||||
| {{ event.description }} | {{ event.description }} | ||||||
| <hr> | <hr> | ||||||
| <h2>{% trans %}Invited users{% endtrans %}</h2> | <h2>{% trans %}Invited users{% endtrans %}</h2> | ||||||
| @@ -25,13 +39,12 @@ | |||||||
| </ul> | </ul> | ||||||
| <hr> | <hr> | ||||||
| <h2>{% trans %}Invite{% endtrans %}</h2> | <h2>{% trans %}Invite{% endtrans %}</h2> | ||||||
| <form method="post"> | <form method="post" class="ui form"> | ||||||
|     {{ form.hidden_tag() }} |     {{ form.hidden_tag() }} | ||||||
|  |     <div class="inline fields"> | ||||||
|  |         {{ field(form.invitee, inline=true) }} | ||||||
|  |  | ||||||
|     {{ form.invitee.errors }} |         <button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button> | ||||||
|     {{ form.invitee.label }} |     </div> | ||||||
|     {{ form.invitee}} |  | ||||||
|  |  | ||||||
|     <button type="submit">{% trans %}Invite{% endtrans %}</button> |  | ||||||
| </form> | </form> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -1,43 +1,27 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <form method="post"> | <h2>Create event</h2> | ||||||
|  | <form method="post" class="ui form"> | ||||||
|     {{ form.hidden_tag() }} |     {{ form.hidden_tag() }} | ||||||
|  |  | ||||||
|     {{ form.errors }} |     {% if form.errors %} | ||||||
|  |         {% for error in form.errors %} | ||||||
|  |     {{ error }} | ||||||
|  |         {% endfor %} | ||||||
|     <br> |     <br> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|     {{ form.title.errors }} |     {{ field(form.title) }} | ||||||
|     {{ form.title.label }} |     {{ field(form.time_zone) }} | ||||||
|     {{ form.title }} |     {{ field(form.start_time) }} | ||||||
|     <br> |     {{ field(form.end_time) }} | ||||||
|  |     {{ field(form.all_day) }} | ||||||
|  |     {{ field(form.description) }} | ||||||
|  |     {{ field(form.visibility) }} | ||||||
|  |  | ||||||
|     {{ form.time_zone.errors }} |     <button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button> | ||||||
|     {{ form.time_zone.label }} |     <a href="{{ url_for('hello') }}" class="ui button">Cancel</a> | ||||||
|     {{ form.time_zone }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.start_time.errors }} |  | ||||||
|     {{ form.start_time.label }} |  | ||||||
|     {{ form.start_time }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.end_time.errors }} |  | ||||||
|     {{ form.end_time.label }} |  | ||||||
|     {{ form.end_time }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.all_day.errors }} |  | ||||||
|     {{ form.all_day.label }} |  | ||||||
|     {{ form.all_day }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.description.errors }} |  | ||||||
|     {{ form.description.label }} |  | ||||||
|     {{ form.description }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     <button type="submit">{% trans %}Save{% endtrans %}</button> |  | ||||||
|     <a href="{{ url_for('hello') }}">Cancel</a> |  | ||||||
| </form> | </form> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,31 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <h1>{% trans %}First steps{% endtrans %}</h1> |  | ||||||
| <p> |  | ||||||
|     {% trans %}Welcome to Calendar.social!{% endtrans %} |  | ||||||
| </p> |  | ||||||
| <p> |  | ||||||
|     {% trans %}These are the first steps you should make before you can start using the site.{% endtrans %} |  | ||||||
| </p> |  | ||||||
| <form method="post"> |  | ||||||
|     {{ form.errors }} |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     <p> |  | ||||||
|         {{ form.display_name.errors }} |  | ||||||
|         {{ form.display_name.label }} |  | ||||||
|         {{ form.display_name }}<br> |  | ||||||
|         {{ form.display_name.description }} |  | ||||||
|     </p> |  | ||||||
|  |  | ||||||
|     <p> |  | ||||||
|         {{ form.time_zone.errors }} |  | ||||||
|         {{ form.time_zone.label }} |  | ||||||
|         {{ form.time_zone }}<br> |  | ||||||
|         {{ form.time_zone.description }} |  | ||||||
|     </p> |  | ||||||
|  |  | ||||||
|     <button type="submit">{% trans %}Save{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock content %} |  | ||||||
| @@ -1,9 +1,11 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  |  | ||||||
| {% block content %} | {% 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' %} | {% 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 %} | {% endblock content %} | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								calsocial/templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								calsocial/templates/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | {# | ||||||
|  |    FIXME: This template should live under security/ if the app templates would override extension | ||||||
|  |           templates… | ||||||
|  | #} | ||||||
|  | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import field %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <h1>{% trans %}Login{% endtrans %}</h1> | ||||||
|  | <form method="post" class="ui form"> | ||||||
|  |     {{ login_user_form.hidden_tag() }} | ||||||
|  |  | ||||||
|  |     {% if login_user_form.errors %} | ||||||
|  |         {% for error in login_user_form.errors %} | ||||||
|  |     {{ error }} | ||||||
|  |         {% endfor %} | ||||||
|  |     <br> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     {{ field(login_user_form.email) }} | ||||||
|  |     {{ field(login_user_form.password) }} | ||||||
|  |     {{ field(login_user_form.remember) }} | ||||||
|  |  | ||||||
|  |     <button type="submit" class="ui primary button">{% trans %}Login{% endtrans %}</button> | ||||||
|  | </form> | ||||||
|  | {% endblock %} | ||||||
| @@ -1,69 +1,3 @@ | |||||||
| <style> |  | ||||||
|  table.calendar > * { |  | ||||||
|      font-family: sans; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  table.calendar { |  | ||||||
|      width: 100%; |  | ||||||
|      border-collapse: collapse; |  | ||||||
|      table-layout: fixed; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.month > td { |  | ||||||
|      text-align: center; |  | ||||||
|      font-weight: bold; |  | ||||||
|      border-bottom: 2px solid black; |  | ||||||
|      padding-bottom: .5em; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.month > td > a { |  | ||||||
|      font-weight: normal; |  | ||||||
|      color: black; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.month > td.month-name > a { |  | ||||||
|      font-weight: bold; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.days > td { |  | ||||||
|      text-align: center; |  | ||||||
|      border-bottom: 2px solid black; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.week > td { |  | ||||||
|      height: 3em; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.week > td > span.day-num { |  | ||||||
|      font-weight: bold; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.week > td.other-month > span.day-num { |  | ||||||
|      font-weight: normal; |  | ||||||
|      color: #909090; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.week > td.today { |  | ||||||
|      background-color: #d8d8d8; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.week > td > a.event { |  | ||||||
|      display: block; |  | ||||||
|      color: black; |  | ||||||
|      text-decoration: none; |  | ||||||
|      border: 1px solid green; |  | ||||||
|      background-color: white; |  | ||||||
|      border-radius: 2px; |  | ||||||
|      text-overflow: ellipsis; |  | ||||||
|      white-space: nowrap; |  | ||||||
|      overflow: hidden; |  | ||||||
|  } |  | ||||||
|  |  | ||||||
|  tr.sizer > td { |  | ||||||
|      width: 14.2857%; |  | ||||||
|      height: 0; |  | ||||||
|  } |  | ||||||
| </style> |  | ||||||
| <table class="calendar"> | <table class="calendar"> | ||||||
|     <thead> |     <thead> | ||||||
|         <tr class="sizer"> |         <tr class="sizer"> | ||||||
| @@ -77,10 +11,10 @@ | |||||||
|         </tr> |         </tr> | ||||||
|         <tr class="month"> |         <tr class="month"> | ||||||
|             <td> |             <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> | ||||||
|             <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> | ||||||
|             <td colspan="3" class="month-name"> |             <td colspan="3" class="month-name"> | ||||||
| {% if not calendar.has_today %} | {% if not calendar.has_today %} | ||||||
| @@ -92,10 +26,10 @@ | |||||||
| {% endif %} | {% endif %} | ||||||
|             </td> |             </td> | ||||||
|             <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> | ||||||
|             <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> |             </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr class="days"> |         <tr class="days"> | ||||||
| @@ -120,9 +54,11 @@ | |||||||
|     {%- endif %} |     {%- endif %} | ||||||
|             <td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}"> |             <td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}"> | ||||||
|                 <span class="day-num">{{ day.day }}</span> |                 <span class="day-num">{{ day.day }}</span> | ||||||
|     {% for event in calendar.day_events(day, user=current_user) %} |     {% 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"> |                 <a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event"> | ||||||
|  |         {% if not event.all_day %} | ||||||
|                     {{ event.start_time_for_user(current_user).strftime('%H:%M') }}–{{ event.end_time_for_user(current_user).strftime('%H:%M') }} |                     {{ event.start_time_for_user(current_user).strftime('%H:%M') }}–{{ event.end_time_for_user(current_user).strftime('%H:%M') }} | ||||||
|  |         {% endif %} | ||||||
|                     {{ event.title }} |                     {{ event.title }} | ||||||
|                 </a> |                 </a> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|   | |||||||
| @@ -1,10 +1,18 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import profile_link %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h1> | <h2 class="ui header"> | ||||||
|     {{ profile.name }} |     {% 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> |     <small>@{{ profile.user.username}}</small> | ||||||
| </h1> | </h2> | ||||||
|     {% if profile.user != current_user %} |     {% if profile.user != current_user %} | ||||||
| <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> | <a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| @@ -14,7 +22,7 @@ | |||||||
| </h2> | </h2> | ||||||
|  |  | ||||||
|     {% for followed in profile.followed_list %} |     {% for followed in profile.followed_list %} | ||||||
| {{ followed }} | {{ profile_link(followed) }} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|  |  | ||||||
| <h2> | <h2> | ||||||
| @@ -22,6 +30,6 @@ | |||||||
| </h2> | </h2> | ||||||
|  |  | ||||||
|     {% for follower in profile.follower_list %} |     {% for follower in profile.follower_list %} | ||||||
| {{ follower }} | {{ profile_link(follower) }} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,30 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <form method="post"> |  | ||||||
|     {{ form.errors }} |  | ||||||
|     {{ form.hidden_tag() }} |  | ||||||
|  |  | ||||||
|     {{ form.username.errors }} |  | ||||||
|     {{ form.username.label }} |  | ||||||
|     {{ form.username }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.email.errors }} |  | ||||||
|     {{ form.email.label }} |  | ||||||
|     {{ form.email }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.password.errors }} |  | ||||||
|     {{ form.password.label }} |  | ||||||
|     {{ form.password }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     {{ form.password_retype.errors }} |  | ||||||
|     {{ form.password_retype.label }} |  | ||||||
|     {{ form.password_retype }} |  | ||||||
|     <br> |  | ||||||
|  |  | ||||||
|     <button type="submit">{% trans %}Register{% endtrans %}</button> |  | ||||||
| </form> |  | ||||||
| {% endblock %} |  | ||||||
| @@ -1,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 %} |  | ||||||
| @@ -1,5 +1,83 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
|  | {% from '_macros.html' import profile_link %} | ||||||
|  |  | ||||||
| {% block content %} | {% 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 %} | {% endblock content %} | ||||||
|   | |||||||
| @@ -1,14 +1,15 @@ | |||||||
| # Hungarian translations for PROJECT. | # Hungarian translations for Calendar.social. | ||||||
| # Copyright (C) 2018 ORGANIZATION | # Copyright (C) 2018 Gergely Polonkai | ||||||
| # This file is distributed under the same license as the PROJECT project. | # This file is distributed under the same license as the Calendar.social | ||||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, 2018. | # project. | ||||||
|  | # Gergely Polonkai <gergely@polonkai.eu>, 2018. | ||||||
| # | # | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version:  0.1\n" | "Project-Id-Version:  0.1\n" | ||||||
| "Report-Msgid-Bugs-To: gergely@polonkai.eu\n" | "Report-Msgid-Bugs-To: gergely@polonkai.eu\n" | ||||||
| "POT-Creation-Date: 2018-06-29 14:14+0200\n" | "POT-Creation-Date: 2018-07-16 11:09+0200\n" | ||||||
| "PO-Revision-Date: 2018-06-29 14:26+0200\n" | "PO-Revision-Date: 2018-07-16 10:37+0200\n" | ||||||
| "Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n" | "Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n" | ||||||
| "Language: hu\n" | "Language: hu\n" | ||||||
| "Language-Team: hu <gergely@polonkai.eu>\n" | "Language-Team: hu <gergely@polonkai.eu>\n" | ||||||
| @@ -18,44 +19,407 @@ msgstr "" | |||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Generated-By: Babel 2.6.0\n" | "Generated-By: Babel 2.6.0\n" | ||||||
|  |  | ||||||
| #: 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" | msgid "Username" | ||||||
| msgstr "Felhasználónév" | msgstr "Felhasználónév" | ||||||
|  |  | ||||||
| #: app/forms.py:9 | #: calsocial/forms.py:97 | ||||||
| msgid "Email address" | msgid "Email address" | ||||||
| msgstr "E-mail cím" | msgstr "E-mail cím" | ||||||
|  |  | ||||||
| #: app/forms.py:10 | #: calsocial/forms.py:98 | ||||||
| msgid "Password" | msgid "Password" | ||||||
| msgstr "Jelszó" | msgstr "Jelszó" | ||||||
|  |  | ||||||
| #: app/forms.py:11 | #: calsocial/forms.py:99 | ||||||
| msgid "Password, once more" | msgid "Password, once more" | ||||||
| msgstr "Jelszó még egszer" | msgstr "Jelszó még egszer" | ||||||
|  |  | ||||||
| #: app/forms.py:15 | #: calsocial/forms.py:108 | ||||||
| msgid "The two passwords must match!" | msgid "The two passwords must match!" | ||||||
| msgstr "A két jelszónak egyeznie kell!" | msgstr "A két jelszónak egyeznie kell!" | ||||||
|  |  | ||||||
| #: 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 | #, python-format | ||||||
| msgid "Logged in as %(username)s" | msgid "Logged in as %(username)s" | ||||||
| msgstr "Bejelentkezve %(username)s néven" | msgstr "Bejelentkezve %(username)s néven" | ||||||
|  |  | ||||||
| #: app/templates/base.html:25 | #: calsocial/templates/base.html:36 | ||||||
| msgid "Login" | msgid "Calendar view" | ||||||
| msgstr "Bejelentkezés" | 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" | msgid "Logout" | ||||||
| msgstr "Kijelentkezés" | 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 | #, python-format | ||||||
| msgid "Welcome to Calendar.social, %(username)s!" | msgid "Welcome to Calendar.social, %(username)s!" | ||||||
| msgstr "Üdv a Calendar.social-ben, %(username)s!" | msgstr "Üdv a Calendar.social-ben, %(username)s!" | ||||||
|  |  | ||||||
| #: 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" | msgid "Register" | ||||||
| msgstr "Regisztráció" | msgstr "Regisztráció" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:18 | ||||||
|  | msgid "Or" | ||||||
|  | msgstr "Vagy" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:20 | ||||||
|  | msgid "Register an account" | ||||||
|  | msgstr "Regisztrálj egy fiókot" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:23 | ||||||
|  | msgid "What is Calendar.social?" | ||||||
|  | msgstr "Mi az a Calendar.social?" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:25 | ||||||
|  | msgid "" | ||||||
|  | "Calendar.social is a calendar app based on open protocols and free, open " | ||||||
|  | "source software." | ||||||
|  | msgstr "" | ||||||
|  | "A Calendar.social egy naptár alkalmazás, mely nyílt protokollokat és " | ||||||
|  | "szabad, nyílt forráskódú szoftvereket használ." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:26 | ||||||
|  | msgid "It is decentralised like one of its counterparts, email." | ||||||
|  | msgstr "Decentralizált, mint legismertebb társa, az e-mail." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:30 | ||||||
|  | msgid "Peek inside" | ||||||
|  | msgstr "Less be" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:36 | ||||||
|  | msgid "Built for users in mind" | ||||||
|  | msgstr "A felhasználók igényeire szabva" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:38 | ||||||
|  | #, fuzzy | ||||||
|  | msgid "" | ||||||
|  | "From planning your appointments to organising large scale events " | ||||||
|  | "Calendar.social can help with all your scheduling needs." | ||||||
|  | msgstr "" | ||||||
|  | "Találkozók tervezésétől a nagyméretű rendezvények szervezéséig, a " | ||||||
|  | "Calendar.social segít az ütemezésben." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:47 | ||||||
|  | msgid "user" | ||||||
|  | msgid_plural "users" | ||||||
|  | msgstr[0] "felhasználó" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:53 | ||||||
|  | msgid "event" | ||||||
|  | msgid_plural "events" | ||||||
|  | msgstr[0] "esemény" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:60 | ||||||
|  | msgid "Built for people" | ||||||
|  | msgstr "Embereknek készítve" | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:62 | ||||||
|  | msgid "Calendar.social is not a commercial network." | ||||||
|  | msgstr "A Calendar.social nem egy kereskedelmi hálózat." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:63 | ||||||
|  | msgid "No advertising, no data mining, no walled gardens." | ||||||
|  | msgstr "Nincs reklám, nincs adatbányászat, nincsenek korlátok." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:64 | ||||||
|  | msgid "There is no central authority." | ||||||
|  | msgstr "Nincs központi autoritás." | ||||||
|  |  | ||||||
|  | #: calsocial/templates/welcome.html:69 | ||||||
|  | msgid "Administered by" | ||||||
|  | msgstr "Üzemelteti" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										462
									
								
								calsocial/translations/it/LC_MESSAGES/messages.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										462
									
								
								calsocial/translations/it/LC_MESSAGES/messages.po
									
									
									
									
									
										Normal 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 "Can’t 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" | ||||||
|  |  | ||||||
							
								
								
									
										466
									
								
								calsocial/translations/pl/LC_MESSAGES/messages.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										466
									
								
								calsocial/translations/pl/LC_MESSAGES/messages.po
									
									
									
									
									
										Normal 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 "Can’t 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ę" | ||||||
| @@ -68,3 +68,54 @@ def force_locale(locale): | |||||||
|         babel.locale_selector_func = orig_locale_selector_func |         babel.locale_selector_func = orig_locale_selector_func | ||||||
|         for key, value in orig_attrs.items(): |         for key, value in orig_attrs.items(): | ||||||
|             setattr(ctx, key, value) |             setattr(ctx, key, value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RoutedMixin: | ||||||
|  |     """Mixin to lazily register class methods as routes | ||||||
|  |  | ||||||
|  |     Works both for `Flask` and `Blueprint` objects. | ||||||
|  |  | ||||||
|  |     Example:: | ||||||
|  |  | ||||||
|  |     class MyBlueprint(Blueprint, RoutedMixin): | ||||||
|  |         def __init__(self, *args, **kwargs): | ||||||
|  |             do_whatever_you_like() | ||||||
|  |  | ||||||
|  |             RoutedMixin.register_routes(self) | ||||||
|  |  | ||||||
|  |         @RoutedMixin.route('/') | ||||||
|  |         def index(self): | ||||||
|  |             return 'Hello, World!' | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def register_routes(self): | ||||||
|  |         """Register all routes that were marked with :meth:`route` | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         for attr_name in self.__dir__(): | ||||||
|  |             attr = getattr(self, attr_name) | ||||||
|  |  | ||||||
|  |             if not callable(attr): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             args, kwargs = getattr(attr, 'routing', (None, None)) | ||||||
|  |  | ||||||
|  |             if args is None: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             self.route(*args, **kwargs)(attr) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def route(*args, **kwargs): | ||||||
|  |         """Mark a function as a future route | ||||||
|  |  | ||||||
|  |         Such functions will be iterated over when the application is initialised.  ``*args`` and | ||||||
|  |         ``**kwargs`` will be passed verbatim to `Flask.route()`. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         def decorator(func):  # pylint: disable=missing-docstring | ||||||
|  |             setattr(func, 'routing', (args, kwargs)) | ||||||
|  |  | ||||||
|  |             return func | ||||||
|  |  | ||||||
|  |         return decorator | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								tests/conftest.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										60
									
								
								tests/helpers.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										49
									
								
								tests/test_app_state.py
									
									
									
									
									
										Normal 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' | ||||||
| @@ -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 pytest | ||||||
|  |  | ||||||
| import calsocial |  | ||||||
| from calsocial.models import db, User |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture |  | ||||||
| def client(): |  | ||||||
|     calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' |  | ||||||
|     calsocial.app.config['TESTING'] = True |  | ||||||
|     calsocial.app.config['WTF_CSRF_ENABLED'] = False |  | ||||||
|     client = calsocial.app.test_client() |  | ||||||
|  |  | ||||||
|     with calsocial.app.app_context(): |  | ||||||
|         db.create_all() |  | ||||||
|  |  | ||||||
|     yield client |  | ||||||
|  |  | ||||||
|     with calsocial.app.app_context(): |  | ||||||
|         db.drop_all() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_index_no_login(client): | def test_index_no_login(client): | ||||||
|     """Test the main page without logging in |     """Test the main page without logging in | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.get('/') |     page = client.get('/') | ||||||
|     assert b'Welcome to Calendar.social' in page.data |     assert b'Peek inside' in page.data | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_register_page(client): | def test_instance_adin_unset(database): | ||||||
|     """Test the registration page |     """Test the instance admin feature if the admin is not set | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.get('/register') |     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'): | ||||||
|     assert b'Register</button>' in page.data |         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={}) |     from calsocial.models import AppState | ||||||
|     assert b'This field is required' in page.data |  | ||||||
|  |  | ||||||
| def test_register_invalid_email(client): |     AppState['instance_admin'] = 'value' | ||||||
|     """Test sending an invalid email address |  | ||||||
|  |     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'): | ||||||
|  |         assert current_app.instance_admin is None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_instance_admin_doesnot_exist(database): | ||||||
|  |     """Test the instance admin feature if the admin user does not exist | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.post('/register', data={ |     from calsocial.models import AppState | ||||||
|         'username': 'test', |  | ||||||
|         'email': 'test', |  | ||||||
|         'password': 'password', |  | ||||||
|         'password_retype': 'password', |  | ||||||
|     }) |  | ||||||
|     assert b'Invalid email address' in page.data |  | ||||||
|  |  | ||||||
| def test_register_password_mismatch(client): |     AppState['instance_admin'] = '0' | ||||||
|     """Test sending different password for registration |  | ||||||
|  |     with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'): | ||||||
|  |         assert current_app.instance_admin is None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_instance_admin(database): | ||||||
|  |     """Test the instance admin feature if the admin user does not exist | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     page = client.post('/register', data={ |     from calsocial.models import db, AppState, User | ||||||
|         '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): |     user = User(username='admin') | ||||||
|     """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') |  | ||||||
|     db.session.add(user) |     db.session.add(user) | ||||||
|     db.session.commit() |     db.session.commit() | ||||||
|  |  | ||||||
|     page = client.post('/register', data={ |     AppState['instance_admin'] = user.id | ||||||
|         '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): |     assert current_app.instance_admin == user | ||||||
|     """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 |  | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								tests/test_follow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								tests/test_follow.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										92
									
								
								tests/test_gregorian.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										76
									
								
								tests/test_login.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										121
									
								
								tests/test_register.py
									
									
									
									
									
										Normal 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 | ||||||
		Reference in New Issue
	
	Block a user