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__/
|
||||
/calsocial/local.db
|
||||
/messages.pot
|
||||
/app/translations/*/LC_MESSAGES/*.mo
|
||||
/calsocial/translations/*/LC_MESSAGES/*.mo
|
||||
/.pytest_cache/
|
||||
/.env
|
||||
/.coverage
|
||||
/htmlcov/
|
||||
|
2
Pipfile
2
Pipfile
@ -13,10 +13,12 @@ sqlalchemy-utils = "*"
|
||||
bcrypt = "*"
|
||||
flask-babelex = "*"
|
||||
python-dateutil = "*"
|
||||
flask-caching = "*"
|
||||
|
||||
[dev-packages]
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
107
Pipfile.lock
generated
107
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3620d7a03e2f49bbf1b812fee29e163e2e0120cd1a3924f6895d3194583e7ac7"
|
||||
"sha256": "01a306fc25c75731af3fcf119a20d92c24fe5be9ddd8be2901b830df10bfb294"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -128,6 +128,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.9.3"
|
||||
},
|
||||
"flask-caching": {
|
||||
"hashes": [
|
||||
"sha256:44fe827c6cc519d48fb0945fa05ae3d128af9a98f2a6e71d4702fd512534f227",
|
||||
"sha256:e34f24631ba240e09fe6241e1bf652863e0cff06a1a94598e23be526bc2e4985"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"flask-login": {
|
||||
"hashes": [
|
||||
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
|
||||
@ -247,9 +255,9 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957"
|
||||
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
|
||||
],
|
||||
"version": "==1.2.9"
|
||||
"version": "==1.2.10"
|
||||
},
|
||||
"sqlalchemy-utils": {
|
||||
"hashes": [
|
||||
@ -276,10 +284,10 @@
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a",
|
||||
"sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a"
|
||||
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d",
|
||||
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f"
|
||||
],
|
||||
"version": "==1.6.5"
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
@ -295,6 +303,50 @@
|
||||
],
|
||||
"version": "==18.1.0"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
|
||||
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
|
||||
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
|
||||
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
|
||||
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
|
||||
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
|
||||
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
|
||||
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
|
||||
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
|
||||
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
|
||||
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
|
||||
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
|
||||
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
|
||||
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
|
||||
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
|
||||
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
|
||||
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
|
||||
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
|
||||
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
|
||||
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
|
||||
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
|
||||
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
|
||||
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
|
||||
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
|
||||
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
|
||||
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
|
||||
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
|
||||
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
|
||||
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
|
||||
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
|
||||
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
|
||||
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
|
||||
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
|
||||
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
|
||||
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
|
||||
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
|
||||
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
|
||||
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
|
||||
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
|
||||
],
|
||||
"version": "==4.5.1"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
|
||||
@ -369,11 +421,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c",
|
||||
"sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3"
|
||||
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434",
|
||||
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.2"
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
@ -383,6 +435,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==3.6.3"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
|
||||
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
@ -390,6 +450,35 @@
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
|
||||
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
|
||||
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
|
||||
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
|
||||
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
|
||||
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
|
||||
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
|
||||
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
|
||||
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
|
||||
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
|
||||
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
|
||||
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
|
||||
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
|
||||
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
|
||||
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
|
||||
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
|
||||
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
|
||||
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
|
||||
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
|
||||
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
|
||||
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
|
||||
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
|
||||
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
|
||||
],
|
||||
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
|
||||
|
@ -19,12 +19,18 @@
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
from warnings import warn
|
||||
|
||||
from flask import Flask, abort, current_app, redirect, render_template, request, url_for
|
||||
from flask import Flask, abort, current_app, has_app_context, redirect, render_template, request, \
|
||||
url_for
|
||||
from flask_babelex import Babel, get_locale as babel_get_locale
|
||||
from flask_security import SQLAlchemyUserDatastore, current_user, login_required
|
||||
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
|
||||
|
||||
from calsocial.account import AccountBlueprint
|
||||
from calsocial.cache import CachedSessionInterface, cache
|
||||
from calsocial.utils import RoutedMixin
|
||||
|
||||
|
||||
def get_locale():
|
||||
"""Locale selector
|
||||
@ -53,22 +59,7 @@ def template_vars():
|
||||
}
|
||||
|
||||
|
||||
def route(*args, **kwargs):
|
||||
"""Mark a function as a future route
|
||||
|
||||
Such functions will be iterated over when the application is initialised. ``*args`` and
|
||||
``**kwargs`` will be passed verbatim to `Flask.route()`.
|
||||
"""
|
||||
|
||||
def decorator(func): # pylint: disable=missing-docstring
|
||||
setattr(func, 'routing', (args, kwargs))
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class CalendarSocialApp(Flask):
|
||||
class CalendarSocialApp(Flask, RoutedMixin):
|
||||
"""The Calendar.social app
|
||||
"""
|
||||
|
||||
@ -79,13 +70,32 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
Flask.__init__(self, name)
|
||||
|
||||
self.session_interface = CachedSessionInterface()
|
||||
|
||||
self._timezone = None
|
||||
|
||||
config_name = os.environ.get('ENV', config or 'dev')
|
||||
config_name = os.environ.get('FLASK_ENV', config or 'development')
|
||||
self.config.from_pyfile(f'config_{config_name}.py', True)
|
||||
# Make sure we look up users both by their usernames and email addresses
|
||||
self.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username', 'email')
|
||||
self.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'login.html'
|
||||
|
||||
# The builtin avatars to use
|
||||
self.config['BUILTIN_AVATARS'] = (
|
||||
'doctor',
|
||||
'engineer',
|
||||
'scientist',
|
||||
'statistician',
|
||||
'user',
|
||||
'whoami',
|
||||
)
|
||||
|
||||
self.jinja_env.policies['ext.i18n.trimmed'] = True # pylint: disable=no-member
|
||||
|
||||
db.init_app(self)
|
||||
|
||||
cache.init_app(self)
|
||||
|
||||
babel = Babel(app=self)
|
||||
babel.localeselector(get_locale)
|
||||
|
||||
@ -96,18 +106,9 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
self.context_processor(template_vars)
|
||||
|
||||
for attr_name in self.__dir__():
|
||||
attr = getattr(self, attr_name)
|
||||
RoutedMixin.register_routes(self)
|
||||
|
||||
if not callable(attr):
|
||||
continue
|
||||
|
||||
args, kwargs = getattr(attr, 'routing', (None, None))
|
||||
|
||||
if args is None:
|
||||
continue
|
||||
|
||||
self.route(*args, **kwargs)(attr)
|
||||
AccountBlueprint().init_app(self, '/accounts/')
|
||||
|
||||
self.before_request(self.goto_first_steps)
|
||||
|
||||
@ -118,8 +119,8 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
if current_user.is_authenticated and \
|
||||
not current_user.profile and \
|
||||
request.endpoint != 'first_steps':
|
||||
return redirect(url_for('first_steps'))
|
||||
request.endpoint != 'account.first_steps':
|
||||
return redirect(url_for('account.first_steps'))
|
||||
|
||||
return None
|
||||
|
||||
@ -128,9 +129,6 @@ class CalendarSocialApp(Flask):
|
||||
"""The default time zone of the app
|
||||
"""
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from flask import has_app_context
|
||||
from pytz import timezone, utc
|
||||
from pytz.exceptions import UnknownTimeZoneError
|
||||
|
||||
@ -149,61 +147,88 @@ class CalendarSocialApp(Flask):
|
||||
|
||||
return self._timezone
|
||||
|
||||
@staticmethod
|
||||
@route('/')
|
||||
def hello():
|
||||
"""View for the main page
|
||||
|
||||
This will display a welcome message for users not logged in; for others, their main
|
||||
calendar view is displayed.
|
||||
@property
|
||||
def instance_admin(self):
|
||||
"""The admin user of this instance
|
||||
"""
|
||||
|
||||
from .calendar_system.gregorian import GregorianCalendar
|
||||
from calsocial.models import AppState, User
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('welcome.html')
|
||||
if not has_app_context():
|
||||
return None
|
||||
|
||||
admin_id = AppState['instance_admin']
|
||||
|
||||
try:
|
||||
admin_id = int(admin_id)
|
||||
except (TypeError, ValueError):
|
||||
warn(f'Instance admin is not set correctly (value is {admin_id})')
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
return User.query.filter(User.id == admin_id).one()
|
||||
except NoResultFound:
|
||||
warn(f'Instance admin is not set correctly (value is {admin_id})')
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _current_calendar():
|
||||
from .calendar_system.gregorian import GregorianCalendar
|
||||
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
|
||||
except TypeError:
|
||||
timestamp = datetime.utcnow()
|
||||
|
||||
calendar = GregorianCalendar(timestamp.timestamp())
|
||||
return GregorianCalendar(timestamp.timestamp())
|
||||
|
||||
return render_template('index.html', calendar=calendar)
|
||||
|
||||
@staticmethod
|
||||
@route('/register', methods=['POST', 'GET'])
|
||||
def register():
|
||||
"""View for user registration
|
||||
|
||||
If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the
|
||||
registration disabled template. Otherwise, it performs user registration.
|
||||
@RoutedMixin.route('/about')
|
||||
def about(self):
|
||||
"""View for the about page
|
||||
"""
|
||||
|
||||
if not current_app.config['REGISTRATION_ENABLED']:
|
||||
return render_template('registration-disabled.html')
|
||||
from .models import User, Event
|
||||
|
||||
from .forms import RegistrationForm
|
||||
from .models import db, User
|
||||
calendar = self._current_calendar()
|
||||
|
||||
form = RegistrationForm()
|
||||
if not current_user.is_authenticated:
|
||||
login_form_class = current_app.extensions['security'].login_form
|
||||
login_form = login_form_class()
|
||||
else:
|
||||
login_form = None
|
||||
|
||||
if form.validate_on_submit():
|
||||
# TODO: This might become False later, if we want registrations to be confirmed via
|
||||
# e-mail
|
||||
user = User(active=True)
|
||||
user_count = User.query.count()
|
||||
event_count = Event.query.count()
|
||||
admin_user = current_app.instance_admin
|
||||
admin_profile = None if admin_user is None else admin_user.profile
|
||||
|
||||
form.populate_obj(user)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return render_template('welcome.html',
|
||||
calendar=calendar,
|
||||
user_only=False,
|
||||
login_form=login_form,
|
||||
user_count=user_count,
|
||||
event_count=event_count,
|
||||
admin_profile=admin_profile)
|
||||
|
||||
return redirect(url_for('hello'))
|
||||
@RoutedMixin.route('/')
|
||||
def hello(self):
|
||||
"""View for the main page
|
||||
|
||||
return render_template('registration.html', form=form)
|
||||
This will display a welcome message for users not logged in; for others, their main
|
||||
calendar view is displayed.
|
||||
"""
|
||||
|
||||
calendar = self._current_calendar()
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return self.about()
|
||||
|
||||
return render_template('index.html', calendar=calendar, user_only=True)
|
||||
|
||||
@staticmethod
|
||||
@route('/new-event', methods=['GET', 'POST'])
|
||||
@RoutedMixin.route('/new-event', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_event():
|
||||
"""View for creating a new event
|
||||
@ -217,7 +242,7 @@ class CalendarSocialApp(Flask):
|
||||
form = EventForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
event = Event(user=current_user)
|
||||
event = Event(profile=current_user.profile)
|
||||
form.populate_obj(event)
|
||||
|
||||
db.session.add(event)
|
||||
@ -228,34 +253,13 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('event-edit.html', form=form)
|
||||
|
||||
@staticmethod
|
||||
@route('/settings', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def settings():
|
||||
"""View for user settings
|
||||
"""
|
||||
|
||||
from .forms import SettingsForm
|
||||
from .models import db
|
||||
|
||||
form = SettingsForm(current_user)
|
||||
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(current_user)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for('hello'))
|
||||
|
||||
return render_template('user-settings.html', form=form)
|
||||
|
||||
@staticmethod
|
||||
@route('/event/<string:event_uuid>', methods=['GET', 'POST'])
|
||||
@RoutedMixin.route('/event/<string:event_uuid>', methods=['GET', 'POST'])
|
||||
def event_details(event_uuid):
|
||||
"""View to display event details
|
||||
"""
|
||||
|
||||
from .forms import InviteForm
|
||||
from .models import db, Event, Invitation, Notification, NotificationAction
|
||||
from .models import db, Event
|
||||
|
||||
try:
|
||||
event = Event.query.filter(Event.event_uuid == event_uuid).one()
|
||||
@ -267,15 +271,7 @@ class CalendarSocialApp(Flask):
|
||||
form = InviteForm(event)
|
||||
|
||||
if form.validate_on_submit():
|
||||
invite = Invitation(event=event, sender=current_user.profile)
|
||||
form.populate_obj(invite)
|
||||
db.session.add(invite)
|
||||
|
||||
notification = Notification(profile=form.invitee.data,
|
||||
actor=current_user.profile,
|
||||
item=event,
|
||||
action=NotificationAction.invite)
|
||||
db.session.add(notification)
|
||||
event.invite(current_user.profile, invitee=form.invitee.data)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@ -284,7 +280,7 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('event-details.html', event=event, form=form)
|
||||
|
||||
@staticmethod
|
||||
@route('/profile/@<string:username>')
|
||||
@RoutedMixin.route('/profile/@<string:username>')
|
||||
def display_profile(username):
|
||||
"""View to display profile details
|
||||
"""
|
||||
@ -299,12 +295,13 @@ class CalendarSocialApp(Flask):
|
||||
return render_template('profile-details.html', profile=profile)
|
||||
|
||||
@staticmethod
|
||||
@route('/profile/@<string:username>/follow')
|
||||
@RoutedMixin.route('/profile/@<string:username>/follow')
|
||||
@login_required
|
||||
def follow_user(username):
|
||||
"""View for following a user
|
||||
"""
|
||||
|
||||
from .models import db, Profile, User, UserFollow, Notification, NotificationAction
|
||||
from .models import db, Profile, User
|
||||
|
||||
try:
|
||||
profile = Profile.query.join(User).filter(User.username == username).one()
|
||||
@ -312,38 +309,14 @@ class CalendarSocialApp(Flask):
|
||||
abort(404)
|
||||
|
||||
if profile.user != current_user:
|
||||
follow = UserFollow(follower=current_user.profile,
|
||||
followed=profile,
|
||||
accepted_at=datetime.utcnow())
|
||||
db.session.add(follow)
|
||||
|
||||
notification = Notification(profile=profile,
|
||||
actor=current_user.profile,
|
||||
item=profile,
|
||||
action=NotificationAction.follow)
|
||||
db.session.add(notification)
|
||||
profile.follow(follower=current_user.profile)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for('display_profile', username=username))
|
||||
|
||||
@staticmethod
|
||||
@route('/notifications')
|
||||
def notifications():
|
||||
"""View to list the notifications for the current user
|
||||
"""
|
||||
|
||||
from .models import Notification
|
||||
|
||||
if current_user.is_authenticated:
|
||||
notifs = Notification.query.filter(Notification.profile == current_user.profile)
|
||||
else:
|
||||
notifs = []
|
||||
|
||||
return render_template('notifications.html', notifs=notifs)
|
||||
|
||||
@staticmethod
|
||||
@route('/accept/<int:invite_id>')
|
||||
@RoutedMixin.route('/accept/<int:invite_id>')
|
||||
def accept_invite(invite_id):
|
||||
"""View to accept an invitation
|
||||
"""
|
||||
@ -373,31 +346,21 @@ class CalendarSocialApp(Flask):
|
||||
return redirect(url_for('event_details', event_uuid=invitation.event.event_uuid))
|
||||
|
||||
@staticmethod
|
||||
@route('/first-steps', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def first_steps():
|
||||
"""View to set up a new registrant’s profile
|
||||
@RoutedMixin.route('/all-events')
|
||||
def all_events():
|
||||
"""View for listing all available events
|
||||
"""
|
||||
|
||||
from .forms import FirstStepsForm
|
||||
from .models import db, Profile
|
||||
from .calendar_system.gregorian import GregorianCalendar
|
||||
|
||||
if current_user.profile:
|
||||
return redirect(url_for('hello'))
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
|
||||
except TypeError:
|
||||
timestamp = datetime.utcnow()
|
||||
|
||||
form = FirstStepsForm()
|
||||
calendar = GregorianCalendar(timestamp.timestamp())
|
||||
|
||||
if form.validate_on_submit():
|
||||
profile = Profile(user=current_user, display_name=form.display_name.data)
|
||||
db.session.add(profile)
|
||||
|
||||
current_user.settings['timezone'] = str(form.time_zone.data)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for('hello'))
|
||||
|
||||
return render_template('first-steps.html', form=form)
|
||||
return render_template('index.html', calendar=calendar, user_only=False)
|
||||
|
||||
|
||||
app = CalendarSocialApp(__name__)
|
||||
|
234
calsocial/account.py
Normal file
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 functools import wraps
|
||||
|
||||
from flask_babelex import lazy_gettext as _
|
||||
|
||||
from . import CalendarSystem
|
||||
|
||||
|
||||
def to_timestamp(func):
|
||||
"""Decorator that converts the return value of a function from `datetime` to a UNIX timestamp
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def _decorator(*args, **kwargs):
|
||||
return func(*args, **kwargs).timestamp()
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
class GregorianCalendar(CalendarSystem):
|
||||
"""Gregorian calendar system for Calendar.social
|
||||
"""
|
||||
@ -83,22 +71,27 @@ class GregorianCalendar(CalendarSystem):
|
||||
def days(self):
|
||||
day_list = []
|
||||
|
||||
start_day = self.timestamp.replace(day=1)
|
||||
month_first = self.timestamp.replace(day=1)
|
||||
|
||||
while start_day.weekday() > self.START_DAY:
|
||||
start_day -= timedelta(days=1)
|
||||
if self.timestamp.month == 12:
|
||||
month_last = month_first.replace(day=31)
|
||||
else:
|
||||
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
|
||||
|
||||
day_list.append(start_day)
|
||||
current_day = start_day
|
||||
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7
|
||||
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7
|
||||
|
||||
while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
|
||||
current_day += timedelta(days=1)
|
||||
day_list.append(current_day)
|
||||
first_display = month_first - timedelta(days=pad_before)
|
||||
last_display = month_last + timedelta(days=pad_after)
|
||||
current = first_display
|
||||
|
||||
while current <= last_display:
|
||||
day_list.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
return day_list
|
||||
|
||||
@property
|
||||
@to_timestamp
|
||||
def prev_year(self):
|
||||
"""Returns the timestamp of the same date in the previous year
|
||||
"""
|
||||
@ -113,7 +106,6 @@ class GregorianCalendar(CalendarSystem):
|
||||
return self.timestamp.replace(year=self.timestamp.year - 1).year
|
||||
|
||||
@property
|
||||
@to_timestamp
|
||||
def prev_month(self):
|
||||
"""Returns the timestamp of the same day in the previous month
|
||||
"""
|
||||
@ -136,7 +128,6 @@ class GregorianCalendar(CalendarSystem):
|
||||
return self.month_names[timestamp.month - 1]
|
||||
|
||||
@property
|
||||
@to_timestamp
|
||||
def next_month(self):
|
||||
"""Returns the timestamp of the same day in the next month
|
||||
"""
|
||||
@ -159,7 +150,6 @@ class GregorianCalendar(CalendarSystem):
|
||||
return self.month_names[timestamp.month - 1]
|
||||
|
||||
@property
|
||||
@to_timestamp
|
||||
def next_year(self):
|
||||
"""Returns the timestamp of the same date in the next year
|
||||
"""
|
||||
@ -192,28 +182,36 @@ class GregorianCalendar(CalendarSystem):
|
||||
|
||||
month_end_timestamp = month_start_timestamp.replace(month=next_month)
|
||||
|
||||
return now >= month_start_timestamp and now < month_end_timestamp
|
||||
return month_start_timestamp <= now < month_end_timestamp
|
||||
|
||||
@staticmethod
|
||||
def day_events(date, user=None):
|
||||
"""Returns all events for a given day
|
||||
"""
|
||||
|
||||
from ..models import Event, Profile
|
||||
from ..models import Event, EventVisibility, Invitation, Profile, Response
|
||||
|
||||
events = Event.query
|
||||
|
||||
if user:
|
||||
events = events.join(Profile) \
|
||||
events = events.outerjoin(Invitation) \
|
||||
.outerjoin(Response) \
|
||||
.join(Profile, Event.profile) \
|
||||
.filter(Profile.user == user)
|
||||
|
||||
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_timestamp = start_timestamp + timedelta(days=1)
|
||||
|
||||
events = events.filter(((Event.start_time >= start_timestamp) &
|
||||
(Event.start_time < end_timestamp)) |
|
||||
((Event.end_time >= start_timestamp) &
|
||||
(Event.end_time < end_timestamp))) \
|
||||
events = events.filter((Event.start_time <= end_timestamp) &
|
||||
(Event.end_time >= start_timestamp)) \
|
||||
.order_by('start_time', 'end_time')
|
||||
|
||||
if user is None:
|
||||
events = events.filter(Event.visibility == EventVisibility.public)
|
||||
else:
|
||||
events = events.filter((Event.visibility == EventVisibility.public) |
|
||||
(Event.profile == user.profile) |
|
||||
(Invitation.invitee == user.profile) |
|
||||
(Response.profile == user.profile))
|
||||
|
||||
return events
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Configuration file for the development environment
|
||||
"""
|
||||
|
||||
ENV = 'dev'
|
||||
ENV = 'development'
|
||||
#: If ``True``, registration on the site is enabled.
|
||||
REGISTRATION_ENABLED = True
|
||||
#: The default time zone
|
||||
@ -14,3 +14,4 @@ SECRET_KEY = 'ThisIsNotSoSecret'
|
||||
SECURITY_PASSWORD_HASH = 'bcrypt'
|
||||
SECURITY_PASSWORD_SALT = SECRET_KEY
|
||||
SECURITY_REGISTERABLE = False
|
||||
CACHE_TYPE = 'simple'
|
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
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from flask_babelex import lazy_gettext as _
|
||||
from flask_security.forms import LoginForm as BaseLoginForm
|
||||
from flask_wtf import FlaskForm
|
||||
import pytz
|
||||
from wtforms import BooleanField, PasswordField, SelectField, StringField
|
||||
from wtforms import BooleanField, PasswordField, SelectField, StringField, RadioField
|
||||
from wtforms.ext.dateutil.fields import DateTimeField
|
||||
from wtforms.validators import DataRequired, Email, StopValidation, ValidationError
|
||||
from wtforms.widgets import TextArea
|
||||
|
||||
from calsocial.models import EventVisibility, EVENT_VISIBILITY_TRANSLATIONS
|
||||
|
||||
class UsernameAvailable(object): # pylint: disable=too-few-public-methods
|
||||
|
||||
class UsernameAvailable: # pylint: disable=too-few-public-methods
|
||||
"""Checks if a username is available
|
||||
"""
|
||||
|
||||
@ -58,7 +62,7 @@ class UsernameAvailable(object): # pylint: disable=too-few-public-methods
|
||||
raise StopValidation(message)
|
||||
|
||||
|
||||
class EmailAvailable(object): # pylint: disable=too-few-public-methods
|
||||
class EmailAvailable: # pylint: disable=too-few-public-methods
|
||||
"""Checks if an email address is available
|
||||
"""
|
||||
|
||||
@ -169,6 +173,45 @@ class TimezoneField(SelectField):
|
||||
yield (value, label, value == self.data)
|
||||
|
||||
|
||||
class EnumField(SelectField):
|
||||
"""Field that allows selecting one value from an ``Enum`` class
|
||||
|
||||
:param enum_type: an ``Enum`` type
|
||||
:type enum_type: type(Enum)
|
||||
:param translations: translatable labels for enum values
|
||||
:type translations: dict
|
||||
:param args: passed verbatim to the constructor of `SelectField`
|
||||
:param kwargs: passed verbatim to the constructor of `SelectField`
|
||||
"""
|
||||
|
||||
def __init__(self, enum_type, translations, *args, **kwargs):
|
||||
if not issubclass(enum_type, Enum):
|
||||
raise TypeError('enum_type must be a subclass of Enum')
|
||||
|
||||
kwargs.update({'choices': [(value, None) for value in enum_type]})
|
||||
self.data = None
|
||||
self.enum_type = enum_type
|
||||
self.translations = translations
|
||||
SelectField.__init__(self, *args, **kwargs)
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
if not valuelist:
|
||||
self.data = None
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
self.data = self.enum_type[valuelist[0]]
|
||||
except KeyError:
|
||||
raise ValueError('Unknown value')
|
||||
|
||||
def iter_choices(self):
|
||||
for value in self.enum_type:
|
||||
label = self.gettext(self.translations[value]) if self.translations else value.name
|
||||
|
||||
yield (value.name, label, value == self.data)
|
||||
|
||||
|
||||
class EventForm(FlaskForm):
|
||||
"""Form for event creation/editing
|
||||
"""
|
||||
@ -179,6 +222,14 @@ class EventForm(FlaskForm):
|
||||
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
|
||||
all_day = BooleanField(_('All day'))
|
||||
description = StringField(_('Description'), widget=TextArea())
|
||||
visibility = EnumField(EventVisibility, EVENT_VISIBILITY_TRANSLATIONS, label=_('Visibility'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from flask_security import current_user
|
||||
|
||||
self.time_zone.kwargs['default'] = current_user.timezone # pylint: disable=no-member
|
||||
|
||||
FlaskForm.__init__(self, *args, **kwargs)
|
||||
|
||||
def populate_obj(self, obj):
|
||||
"""Populate ``obj`` with event data
|
||||
@ -330,8 +381,33 @@ class FirstStepsForm(FlaskForm):
|
||||
display_name = StringField(
|
||||
label=_('Display name'),
|
||||
validators=[DataRequired()],
|
||||
# pylint: disable=line-too-long
|
||||
description=_('This will be shown to other users as your name. You can use your real name, or any nickname you like.'))
|
||||
time_zone = TimezoneField(
|
||||
label=_('Your time zone'),
|
||||
validators=[DataRequired()],
|
||||
description=_('The start and end times of events will be displayed in this time zone.'))
|
||||
|
||||
|
||||
class ProfileForm(FlaskForm):
|
||||
"""Form for editing a user profile
|
||||
"""
|
||||
|
||||
display_name = StringField(label=_('Display name'), validators=[DataRequired()])
|
||||
builtin_avatar = RadioField(label=_('Use a built-in avatar'))
|
||||
locked = BooleanField(label=_('Lock profile'))
|
||||
|
||||
def __init__(self, profile, *args, **kwargs):
|
||||
from flask import current_app
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
'display_name': profile.display_name,
|
||||
'locked': profile.locked,
|
||||
'builtin_avatar': profile.builtin_avatar,
|
||||
})
|
||||
FlaskForm.__init__(self, *args, **kwargs)
|
||||
|
||||
self.builtin_avatar.choices = [(name, name)
|
||||
for name in current_app.config['BUILTIN_AVATARS']]
|
||||
self.profile = profile
|
||||
|
@ -21,12 +21,15 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from warnings import warn
|
||||
|
||||
from flask import current_app
|
||||
from flask_babelex import lazy_gettext
|
||||
from flask_security import UserMixin, RoleMixin
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy_utils.types.choice import ChoiceType
|
||||
|
||||
from .app_state import app_state_base
|
||||
from .cache import cache
|
||||
from .utils import force_locale
|
||||
|
||||
db = SQLAlchemy()
|
||||
@ -98,11 +101,28 @@ class ResponseType(Enum):
|
||||
return self.name.lower() == other.lower() # pylint: disable=no-member
|
||||
|
||||
if isinstance(other, (int, float)):
|
||||
return self.value == other
|
||||
return self.value == other # pylint: disable=comparison-with-callable
|
||||
|
||||
return Enum.__eq__(self, other)
|
||||
|
||||
|
||||
class EventVisibility(Enum):
|
||||
"""Enumeration for event visibility
|
||||
"""
|
||||
|
||||
#: The event is private, only attendees and people invited can see the details
|
||||
private = 0
|
||||
|
||||
#: The event is public, anyone can see the details
|
||||
public = 5
|
||||
|
||||
|
||||
EVENT_VISIBILITY_TRANSLATIONS = {
|
||||
EventVisibility.private: _('Visible only to attendees'),
|
||||
EventVisibility.public: _('Visible to everyone'),
|
||||
}
|
||||
|
||||
|
||||
class SettingsProxy:
|
||||
"""Proxy object to get settings for a user
|
||||
"""
|
||||
@ -188,7 +208,6 @@ class User(db.Model, UserMixin):
|
||||
If the user didn’t set a time zone yet, the application default is used.
|
||||
"""
|
||||
|
||||
from flask import current_app
|
||||
from pytz import timezone
|
||||
from pytz.exceptions import UnknownTimeZoneError
|
||||
|
||||
@ -202,6 +221,24 @@ class User(db.Model, UserMixin):
|
||||
|
||||
return current_app.timezone
|
||||
|
||||
@property
|
||||
def session_list_key(self):
|
||||
"""The cache key of this 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):
|
||||
return f'<User {self.id}({self.username})>'
|
||||
|
||||
@ -246,6 +283,12 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
#: The display name
|
||||
display_name = db.Column(db.Unicode(length=80), nullable=False)
|
||||
|
||||
#: If locked, a profile cannot be followed without the 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
|
||||
def fqn(self):
|
||||
"""The fully qualified name of the profile
|
||||
@ -265,7 +308,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
domain = ''
|
||||
else:
|
||||
username = self.username
|
||||
domain = '@' + self.domain
|
||||
domain = f'@{self.domain}'
|
||||
|
||||
return f'<Profile {self.id}(@{username}{domain})>'
|
||||
|
||||
@ -290,7 +333,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
|
||||
return Profile.query \
|
||||
.join(UserFollow.followed) \
|
||||
.filter(UserFollow.follower == self)
|
||||
.filter(UserFollow.follower == self) \
|
||||
.filter(UserFollow.accepted_at.isnot(None))
|
||||
|
||||
@property
|
||||
def follower_list(self):
|
||||
@ -303,7 +347,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
|
||||
return Profile.query \
|
||||
.join(UserFollow.follower) \
|
||||
.filter(UserFollow.followed == self)
|
||||
.filter(UserFollow.followed == self) \
|
||||
.filter(UserFollow.accepted_at.isnot(None))
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
@ -317,6 +362,48 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
|
||||
return NotImplemented
|
||||
|
||||
def follow(self, follower):
|
||||
"""Make ``follower`` follow this profile
|
||||
"""
|
||||
|
||||
if not isinstance(follower, Profile):
|
||||
raise TypeError('Folloer must be a Profile object')
|
||||
|
||||
timestamp = None if self.locked else datetime.utcnow()
|
||||
|
||||
user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp)
|
||||
db.session.add(user_follow)
|
||||
notification = self.notify(follower, self, NotificationAction.follow)
|
||||
|
||||
db.session.add(notification)
|
||||
|
||||
return user_follow
|
||||
|
||||
def notify(self, actor, item, action):
|
||||
"""Notify this profile about ``action`` on ``item`` by ``actor``
|
||||
|
||||
:param actor: the actor who generated the notification
|
||||
:type actor: Profile
|
||||
:param item: the item ``action`` was performed on
|
||||
:type item: any
|
||||
:param action: the type of the action
|
||||
:type action: NotificationAction, str
|
||||
:raises TypeError: if ``actor`` is not a `Profile` object
|
||||
:returns: the generated notification. It is already added to the database session, but
|
||||
not committed
|
||||
:rtype: Notification
|
||||
"""
|
||||
|
||||
if not isinstance(actor, Profile):
|
||||
raise TypeError('actor must be a Profile instance')
|
||||
|
||||
if isinstance(action, str):
|
||||
action = NotificationAction[action]
|
||||
|
||||
notification = Notification(profile=self, actor=actor, item=item, action=action)
|
||||
|
||||
return notification
|
||||
|
||||
|
||||
class Event(db.Model):
|
||||
"""Database model for events
|
||||
@ -352,6 +439,9 @@ class Event(db.Model):
|
||||
#: The description of the event
|
||||
description = db.Column(db.UnicodeText())
|
||||
|
||||
#: The visibility of the event
|
||||
visibility = db.Column(db.Enum(EventVisibility), nullable=False)
|
||||
|
||||
def __as_tz(self, timestamp, as_timezone=None):
|
||||
from pytz import timezone, utc
|
||||
|
||||
@ -400,6 +490,20 @@ class Event(db.Model):
|
||||
|
||||
return url_for('event_details', event_uuid=self.event_uuid)
|
||||
|
||||
def invite(self, inviter, invited):
|
||||
"""Invite ``invited`` to the event
|
||||
|
||||
The invitation will arrive from ``inviter``.
|
||||
"""
|
||||
|
||||
invite = Invitation(event=self, sender=inviter, invitee=invited)
|
||||
db.session.add(invite)
|
||||
|
||||
notification = invited.notify(inviter, self, NotificationAction.invite)
|
||||
db.session.add(notification)
|
||||
|
||||
return invite
|
||||
|
||||
|
||||
class UserSetting(db.Model): # pylint: disable=too-few-public-methods
|
||||
"""Database model for user settings
|
||||
@ -543,6 +647,12 @@ class UserFollow(db.Model): # pylint: disable=too-few-public-methods
|
||||
#: The timestamp when the follow was accepted
|
||||
accepted_at = db.Column(db.DateTime(), nullable=True)
|
||||
|
||||
def accept(self):
|
||||
"""Accept this follow request
|
||||
"""
|
||||
|
||||
self.accepted_at = datetime.utcnow()
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
"""Database model for notifications
|
||||
@ -680,3 +790,64 @@ class Response(db.Model): # pylint: disable=too-few-public-methods
|
||||
|
||||
#: The response itself
|
||||
response = db.Column(db.Enum(ResponseType), nullable=False)
|
||||
|
||||
|
||||
class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-methods,inherit-non-class
|
||||
"""Database model for application state values
|
||||
"""
|
||||
|
||||
__tablename__ = 'app_state'
|
||||
|
||||
#: The environment that set this key
|
||||
env = db.Column(db.String(length=40), nullable=False, primary_key=True)
|
||||
|
||||
#: The key
|
||||
key = db.Column(db.String(length=80), nullable=False, primary_key=True)
|
||||
|
||||
#: The value of the key
|
||||
value = db.Column(db.Unicode(length=200), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def __get_state__(cls, key):
|
||||
try:
|
||||
record = cls.query \
|
||||
.filter(cls.env == current_app.env) \
|
||||
.filter(cls.key == key) \
|
||||
.one()
|
||||
except NoResultFound:
|
||||
return None
|
||||
|
||||
return record.value
|
||||
|
||||
@classmethod
|
||||
def __set_state__(cls, key, value):
|
||||
try:
|
||||
record = cls.query \
|
||||
.filter(cls.env == current_app.env) \
|
||||
.filter(cls.key == key) \
|
||||
.one()
|
||||
except NoResultFound:
|
||||
record = cls(env=current_app.env, key=key)
|
||||
|
||||
record.value = value
|
||||
db.session.add(record)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def __set_state_default__(cls, key, value):
|
||||
try:
|
||||
record = cls.query \
|
||||
.filter(cls.env == current_app.env) \
|
||||
.filter(cls.key == key) \
|
||||
.one()
|
||||
except NoResultFound:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
record = cls(env=current_app.env, key=key, value=value)
|
||||
db.session.add(record)
|
||||
db.session.commit()
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AppState {self.env}:{self.key}="{self.value}"'
|
||||
|
@ -17,7 +17,7 @@
|
||||
"""Security related things for Calendar.social
|
||||
"""
|
||||
|
||||
from flask import current_app
|
||||
from flask import current_app, session
|
||||
from flask_login.signals import user_logged_in, user_logged_out
|
||||
from flask_security import Security, AnonymousUser as BaseAnonymousUser
|
||||
|
||||
@ -35,6 +35,15 @@ class AnonymousUser(BaseAnonymousUser):
|
||||
|
||||
return current_app.timezone
|
||||
|
||||
@property
|
||||
def profile(self):
|
||||
"""The profile of the anonymous user
|
||||
|
||||
Always evaluates to ``None``
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@user_logged_in.connect
|
||||
def login_handler(app, user): # pylint: disable=unused-argument
|
||||
@ -45,6 +54,8 @@ def login_handler(app, user): # pylint: disable=unused-argument
|
||||
|
||||
AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS)
|
||||
|
||||
user.active_sessions += [session.sid]
|
||||
|
||||
|
||||
@user_logged_out.connect
|
||||
def logout_handler(app, user): # pylint: disable=unused-argument
|
||||
|
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 %}
|
||||
{% for notif in notifs %}
|
||||
{{ notif.html }}<br>
|
||||
{% else %}
|
||||
{% trans %}Nothing to show.{% endtrans %}
|
||||
{% endfor %}
|
||||
{% 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-96.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/calendar-social-icon-192.png') }}">
|
||||
{% block head %}
|
||||
<style>
|
||||
header > h1 > img {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3em;
|
||||
font-weight: bold;
|
||||
border-top: 1px dotted black;
|
||||
padding-top: 1em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.0/css/fork-awesome.min.css" integrity="sha256-sX8HLspqYoXVPetzJRE4wPhIhDBu2NB0kYpufzkQSms=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='semantic/semantic.min.css') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
|
||||
Calendar.social
|
||||
</h1>
|
||||
<nav class="menu">
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ _('Logged in as %(username)s', username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe) }}
|
||||
{% endif %}
|
||||
<ul>
|
||||
<div class="ui top attached menu">
|
||||
<div class="header item">
|
||||
<img src="{{ url_for('static', filename='images/calendar-social-icon.svg') }}">
|
||||
Calendar.social
|
||||
</div>
|
||||
<div class="right menu">
|
||||
{% if not current_user.is_authenticated %}
|
||||
<li><a href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a></li>
|
||||
<a class="item" href="{{ url_for('security.login') }}">{% trans %}Login{% endtrans %}</a>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a></li>
|
||||
<li><a href="{{ url_for('notifications') }}">{% trans %}Notifications{% endtrans %}</a></li>
|
||||
<li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li>
|
||||
<li><a href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a></li>
|
||||
<div class="item">
|
||||
{% trans username=('<a href="' + url_for('display_profile', username=current_user.username) + '">' + current_user.username + '</a>') | safe -%}
|
||||
Logged in as {{username}}
|
||||
{%- endtrans %}
|
||||
</div>
|
||||
<a class="item" href="{{ url_for('hello') }}">{% trans %}Calendar view{% endtrans %}</a>
|
||||
<a class="item" href="{{ url_for('account.notifications') }}">{% trans %}Notifications{% endtrans %}</a>
|
||||
<a class="item" href="{{ url_for('account.settings') }}">{% trans %}Settings{% endtrans %}</a>
|
||||
<a class="item" href="{{ url_for('security.logout') }}">{% trans %}Logout{% endtrans %}</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui container" id="content">
|
||||
{% block content %}{% endblock %}
|
||||
<footer>
|
||||
</div>
|
||||
<footer class="ui segment">
|
||||
<a href="{{ url_for('about') }}">{% trans %}About this instance{% endtrans %}</a><br>
|
||||
Soon…™
|
||||
</footer>
|
||||
{% block scripts %}{% endblock %}
|
||||
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="{{ url_for('static', filename='semantic/semantic.min.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,15 +1,29 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from '_macros.html' import field %}
|
||||
|
||||
{% macro time_zone_warning() %}
|
||||
{% trans timezone=event.time_zone, start_time=event.start_time_tz | datetimeformat(rebase=false), end_time=event.end_time_tz | datetimeformat(rebase=false) -%}
|
||||
This event is organised in the {{timezone}} time zone, in which it happens between {{start_time}} and {{end_time}}
|
||||
{%- endtrans %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{{ event.title }}<br>
|
||||
<small>
|
||||
{{ event.start_time_for_user(current_user) }}–{{ event.end_time_for_user(current_user) }}
|
||||
{% if current_user.timezone | string != event.time_zone %}
|
||||
({{ event.start_time_tz }}–{{ event.end_time_tz }} {{ event.time_zone }})
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
{{ event.title }}<br>
|
||||
<div class="sub header">
|
||||
{%- if current_user.timezone | string != event.time_zone -%}
|
||||
<span title="{{ time_zone_warning() }}">
|
||||
<i class="fa fa-exclamation-triangle timezone-warning"></i>
|
||||
<span class="sr-only">{{ time_zone_warning() }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
</h1>
|
||||
{{ event.start_time_for_user(current_user) | datetimeformat(rebase=false) }}
|
||||
–
|
||||
{{ event.end_time_for_user(current_user) | datetimeformat(rebase=false) }}
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{{ event.description }}
|
||||
<hr>
|
||||
<h2>{% trans %}Invited users{% endtrans %}</h2>
|
||||
@ -25,13 +39,12 @@
|
||||
</ul>
|
||||
<hr>
|
||||
<h2>{% trans %}Invite{% endtrans %}</h2>
|
||||
<form method="post">
|
||||
<form method="post" class="ui form">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="inline fields">
|
||||
{{ field(form.invitee, inline=true) }}
|
||||
|
||||
{{ form.invitee.errors }}
|
||||
{{ form.invitee.label }}
|
||||
{{ form.invitee}}
|
||||
|
||||
<button type="submit">{% trans %}Invite{% endtrans %}</button>
|
||||
<button type="submit" class="ui button">{% trans %}Invite{% endtrans %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -1,43 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from '_macros.html' import field %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<h2>Create event</h2>
|
||||
<form method="post" class="ui form">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ form.errors }}
|
||||
{% if form.errors %}
|
||||
{% for error in form.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{{ form.title.errors }}
|
||||
{{ form.title.label }}
|
||||
{{ form.title }}
|
||||
<br>
|
||||
{{ field(form.title) }}
|
||||
{{ field(form.time_zone) }}
|
||||
{{ field(form.start_time) }}
|
||||
{{ field(form.end_time) }}
|
||||
{{ field(form.all_day) }}
|
||||
{{ field(form.description) }}
|
||||
{{ field(form.visibility) }}
|
||||
|
||||
{{ form.time_zone.errors }}
|
||||
{{ form.time_zone.label }}
|
||||
{{ form.time_zone }}
|
||||
<br>
|
||||
|
||||
{{ form.start_time.errors }}
|
||||
{{ form.start_time.label }}
|
||||
{{ form.start_time }}
|
||||
<br>
|
||||
|
||||
{{ form.end_time.errors }}
|
||||
{{ form.end_time.label }}
|
||||
{{ form.end_time }}
|
||||
<br>
|
||||
|
||||
{{ form.all_day.errors }}
|
||||
{{ form.all_day.label }}
|
||||
{{ form.all_day }}
|
||||
<br>
|
||||
|
||||
{{ form.description.errors }}
|
||||
{{ form.description.label }}
|
||||
{{ form.description }}
|
||||
<br>
|
||||
|
||||
<button type="submit">{% trans %}Save{% endtrans %}</button>
|
||||
<a href="{{ url_for('hello') }}">Cancel</a>
|
||||
<button type="submit" class="ui primary button">{% trans %}Save{% endtrans %}</button>
|
||||
<a href="{{ url_for('hello') }}" class="ui button">Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -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' %}
|
||||
|
||||
{% block content %}
|
||||
{{ _('Welcome to Calendar.social, %(username)s!', username=current_user.username) }}
|
||||
{% trans username=current_user.username -%}
|
||||
Welcome to Calendar.social, {{username}}!
|
||||
{%- endtrans %}
|
||||
|
||||
{% include 'month-view.html' %}
|
||||
|
||||
<a href="{{ url_for('new_event') }}">{% trans %}Add event{% endtrans %}</a>
|
||||
<a href="{{ url_for('new_event') }}" class="ui primary button">{% trans %}Add event{% endtrans %}</a>
|
||||
{% endblock content %}
|
||||
|
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">
|
||||
<thead>
|
||||
<tr class="sizer">
|
||||
@ -77,10 +11,10 @@
|
||||
</tr>
|
||||
<tr class="month">
|
||||
<td>
|
||||
<a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a>
|
||||
<a href="{{ url_for('hello', date=calendar.prev_year.timestamp()) }}">« {{ calendar.prev_year_year }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('hello', date=calendar.prev_month) }}">‹ {{ calendar.prev_month_name }}</a>
|
||||
<a href="{{ url_for('hello', date=calendar.prev_month.timestamp()) }}">‹ {{ calendar.prev_month_name }}</a>
|
||||
</td>
|
||||
<td colspan="3" class="month-name">
|
||||
{% if not calendar.has_today %}
|
||||
@ -92,10 +26,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('hello', date=calendar.next_month) }}">{{ calendar.next_month_name }} ›</a>
|
||||
<a href="{{ url_for('hello', date=calendar.next_month.timestamp()) }}">{{ calendar.next_month_name }} ›</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('hello', date=calendar.next_year) }}">{{ calendar.next_year_year }} »</a>
|
||||
<a href="{{ url_for('hello', date=calendar.next_year.timestamp()) }}">{{ calendar.next_year_year }} »</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="days">
|
||||
@ -120,9 +54,11 @@
|
||||
{%- endif %}
|
||||
<td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}">
|
||||
<span class="day-num">{{ day.day }}</span>
|
||||
{% for event in calendar.day_events(day, user=current_user) %}
|
||||
{% for event in calendar.day_events(day, user=current_user if user_only else none) %}
|
||||
<a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event">
|
||||
{% if not event.all_day %}
|
||||
{{ event.start_time_for_user(current_user).strftime('%H:%M') }}–{{ event.end_time_for_user(current_user).strftime('%H:%M') }}
|
||||
{% endif %}
|
||||
{{ event.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@ -1,10 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from '_macros.html' import profile_link %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{{ profile.name }}
|
||||
<h2 class="ui header">
|
||||
{% if profile.builtin_avatar %}
|
||||
<img src="{{ url_for('static', filename='avatars/' + profile.builtin_avatar + '.svg') }}" alt="" class="ui circular image">
|
||||
{% endif %}
|
||||
{% if profile.locked %}
|
||||
<i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i>
|
||||
<span class="sr-only">{% trans %}locked profile{% endtrans %}</span>
|
||||
{% endif %}
|
||||
{{ profile.display_name }}
|
||||
<small>@{{ profile.user.username}}</small>
|
||||
</h1>
|
||||
</h2>
|
||||
{% if profile.user != current_user %}
|
||||
<a href="{{ url_for('follow_user', username=profile.user.username) }}">{% trans %}Follow{% endtrans %}</a>
|
||||
{% endif %}
|
||||
@ -14,7 +22,7 @@
|
||||
</h2>
|
||||
|
||||
{% for followed in profile.followed_list %}
|
||||
{{ followed }}
|
||||
{{ profile_link(followed) }}
|
||||
{% endfor %}
|
||||
|
||||
<h2>
|
||||
@ -22,6 +30,6 @@
|
||||
</h2>
|
||||
|
||||
{% for follower in profile.follower_list %}
|
||||
{{ follower }}
|
||||
{{ profile_link(follower) }}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
||||
|
@ -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' %}
|
||||
{% from '_macros.html' import profile_link %}
|
||||
|
||||
{% block content %}
|
||||
<p>Welcome to Calendar.social. There will be lot of content here soon!</p>
|
||||
<div class="ui grid">
|
||||
<div class="row">
|
||||
<div class="four wide column">
|
||||
{% if not current_user.is_authenticated %}
|
||||
<h2>{% trans %}Login{% endtrans %}</h2>
|
||||
<form class="ui form" method="post" action="{{ url_for('security.login') }}">
|
||||
{{ login_form.hidden_tag() }}
|
||||
{{ login_form.email.label }}
|
||||
{{ login_form.email }}
|
||||
{{ login_form.password.label }}
|
||||
{{ login_form.password }}
|
||||
<button type="submit" class="fluid ui primary button">{% trans %}Login{% endtrans %}</button>
|
||||
</form>
|
||||
{% if config.REGISTRATION_ENABLED %}
|
||||
<div class="ui horizontal divider">
|
||||
{% trans %}Or{% endtrans %}
|
||||
</div>
|
||||
<a href="{{ url_for('account.register_account' ) }}" class="fluid ui button">{% trans %}Register an account{% endtrans %}</a>
|
||||
{% endif %}
|
||||
<div class="ui horizontal divider"></div>
|
||||
{% endif %}
|
||||
<h2>{% trans %}What is Calendar.social?{% endtrans %}</h2>
|
||||
<p>
|
||||
{% trans %}Calendar.social is a calendar app based on open protocols and free, open source software.{% endtrans %}
|
||||
{% trans %}It is decentralised like one of its counterparts, email.{% endtrans %}
|
||||
</p>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="ui horizontal divider"></div>
|
||||
<a href="{{ url_for('hello') }}" class="ui fluid primary button">{% trans %}Go to your calendar{% endtrans %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="twelve wide column">
|
||||
<h2>{% trans %}Peek inside{% endtrans %}</h2>
|
||||
{% include 'month-view.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="four wide column">
|
||||
<h2>{% trans %}Built with users in mind{% endtrans %}</h2>
|
||||
<p>
|
||||
{% trans %}From planning your appointments to organising large scale events Calendar.social can help with all your scheduling needs.{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column">
|
||||
<div class="ui centered statistics">
|
||||
<div class="statistic">
|
||||
<div class="value">{{ user_count }}</div>
|
||||
<div class="label">
|
||||
{% trans count=user_count %}user{% pluralize %}users{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic">
|
||||
<div class="value">{{ event_count }}</div>
|
||||
<div class="label">
|
||||
{% trans count=event_count %}event{% pluralize %}events{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="four wide column">
|
||||
<h2>{% trans %}Built for people{% endtrans %}</h2>
|
||||
<p>
|
||||
{% trans %}Calendar.social is not a commercial network.{% endtrans %}
|
||||
{% trans %}No advertising, no data mining, no walled gardens.{% endtrans %}
|
||||
{% trans %}There is no central authority.{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column">
|
||||
{% if admin_profile %}
|
||||
<h2>{% trans %}Administered by{% endtrans %}</h2>
|
||||
{{ profile_link(admin_profile) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -1,14 +1,15 @@
|
||||
# Hungarian translations for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
# Hungarian translations for Calendar.social.
|
||||
# Copyright (C) 2018 Gergely Polonkai
|
||||
# This file is distributed under the same license as the Calendar.social
|
||||
# project.
|
||||
# Gergely Polonkai <gergely@polonkai.eu>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1\n"
|
||||
"Project-Id-Version: 0.1\n"
|
||||
"Report-Msgid-Bugs-To: gergely@polonkai.eu\n"
|
||||
"POT-Creation-Date: 2018-06-29 14:14+0200\n"
|
||||
"PO-Revision-Date: 2018-06-29 14:26+0200\n"
|
||||
"POT-Creation-Date: 2018-07-16 11:09+0200\n"
|
||||
"PO-Revision-Date: 2018-07-16 10:37+0200\n"
|
||||
"Last-Translator: Gergely Polonkai <gergely@polonkai.eu>\n"
|
||||
"Language: hu\n"
|
||||
"Language-Team: hu <gergely@polonkai.eu>\n"
|
||||
@ -18,44 +19,407 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.6.0\n"
|
||||
|
||||
#: app/forms.py:8
|
||||
#: calsocial/forms.py:53
|
||||
msgid "This username is not available"
|
||||
msgstr "Ez a felhasználónév nem elérhető"
|
||||
|
||||
#: calsocial/forms.py:84
|
||||
msgid "This email address can not be used"
|
||||
msgstr "Ez az e-mail cím nem használható"
|
||||
|
||||
#: calsocial/forms.py:96
|
||||
msgid "Username"
|
||||
msgstr "Felhasználónév"
|
||||
|
||||
#: app/forms.py:9
|
||||
#: calsocial/forms.py:97
|
||||
msgid "Email address"
|
||||
msgstr "E-mail cím"
|
||||
|
||||
#: app/forms.py:10
|
||||
#: calsocial/forms.py:98
|
||||
msgid "Password"
|
||||
msgstr "Jelszó"
|
||||
|
||||
#: app/forms.py:11
|
||||
#: calsocial/forms.py:99
|
||||
msgid "Password, once more"
|
||||
msgstr "Jelszó még egszer"
|
||||
|
||||
#: app/forms.py:15
|
||||
#: calsocial/forms.py:108
|
||||
msgid "The two passwords must match!"
|
||||
msgstr "A két jelszónak egyeznie kell!"
|
||||
|
||||
#: app/templates/base.html:21
|
||||
#: calsocial/forms.py:176
|
||||
msgid "Title"
|
||||
msgstr "Cím"
|
||||
|
||||
#: calsocial/forms.py:177 calsocial/forms.py:209
|
||||
msgid "Time zone"
|
||||
msgstr "Időzóna"
|
||||
|
||||
#: calsocial/forms.py:178
|
||||
msgid "Start time"
|
||||
msgstr "Kezdés időpontja"
|
||||
|
||||
#: calsocial/forms.py:179
|
||||
msgid "End time"
|
||||
msgstr "Befejezés időpontja"
|
||||
|
||||
#: calsocial/forms.py:180
|
||||
msgid "All day"
|
||||
msgstr "Egész napos"
|
||||
|
||||
#: calsocial/forms.py:181
|
||||
msgid "Description"
|
||||
msgstr "Leírás"
|
||||
|
||||
#: calsocial/forms.py:202
|
||||
msgid "End time must be later than start time!"
|
||||
msgstr "A befejezés időpontjának későbbre kell esni, mint a kezdés időpontjának!"
|
||||
|
||||
#: calsocial/forms.py:233
|
||||
msgid "Username or email"
|
||||
msgstr "Felhasználónév vagy e-mail cím"
|
||||
|
||||
#: calsocial/forms.py:321
|
||||
msgid "User is already invited"
|
||||
msgstr "Ez a felhasználó már meg van hívva"
|
||||
|
||||
#: calsocial/forms.py:331 calsocial/forms.py:345
|
||||
msgid "Display name"
|
||||
msgstr "Megjelenítendő név"
|
||||
|
||||
#: calsocial/forms.py:334
|
||||
msgid ""
|
||||
"This will be shown to other users as your name. You can use your real "
|
||||
"name, or any nickname you like."
|
||||
msgstr ""
|
||||
"Ezt látja majd a többi felhasználó. Megadhatod a valódi neved, vagy a "
|
||||
"kedvenc beceneved."
|
||||
|
||||
#: calsocial/forms.py:336
|
||||
msgid "Your time zone"
|
||||
msgstr "Az időzónád"
|
||||
|
||||
#: calsocial/forms.py:338
|
||||
msgid "The start and end times of events will be displayed in this time zone."
|
||||
msgstr ""
|
||||
"Az események kezdési és befejezési időpontjai eszerint az időzóna szerint"
|
||||
" jelennek majd meg."
|
||||
|
||||
#: calsocial/forms.py:346
|
||||
msgid "Lock profile"
|
||||
msgstr "Profil zárolása"
|
||||
|
||||
#: calsocial/models.py:72
|
||||
#, python-format
|
||||
msgid "%(actor)s followed you"
|
||||
msgstr "%(actor)s követ téged"
|
||||
|
||||
#: calsocial/models.py:72
|
||||
#, python-format
|
||||
msgid "%(actor)s followed %(item)s"
|
||||
msgstr "%(actor)s követi ezt: %(item)s"
|
||||
|
||||
#: calsocial/models.py:73
|
||||
#, python-format
|
||||
msgid "%(actor)s invited you to %(item)s"
|
||||
msgstr "%(actor)s meghívott erre: %(item)s"
|
||||
|
||||
#: calsocial/models.py:499
|
||||
#, python-format
|
||||
msgid "%(user)s logged in"
|
||||
msgstr "%(user)s bejelentkezett"
|
||||
|
||||
#: calsocial/models.py:500
|
||||
#, python-format
|
||||
msgid "%(user)s failed to log in"
|
||||
msgstr "%(user)s nem tudott bejelentkezni"
|
||||
|
||||
#: calsocial/models.py:501
|
||||
#, python-format
|
||||
msgid "%(user)s logged out"
|
||||
msgstr "%(user)s kijelentkezett"
|
||||
|
||||
#: calsocial/models.py:518
|
||||
#, python-format
|
||||
msgid "UNKNOWN RECORD \"%(log_type)s\" for %(user)s"
|
||||
msgstr "ISMERETLEN BEJEGYZÉS TÍPUS (\"%(log_type)s\") %(user)s felhasználótól"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:43
|
||||
msgid "Gregorian"
|
||||
msgstr "Gergely naptár"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:51
|
||||
msgid "January"
|
||||
msgstr "Január"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:52
|
||||
msgid "February"
|
||||
msgstr "Február"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:53
|
||||
msgid "March"
|
||||
msgstr "Március"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:54
|
||||
msgid "April"
|
||||
msgstr "Április"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:55
|
||||
msgid "May"
|
||||
msgstr "Május"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:56
|
||||
msgid "June"
|
||||
msgstr "Június"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:57
|
||||
msgid "July"
|
||||
msgstr "Július"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:58
|
||||
msgid "August"
|
||||
msgstr "Augusztus"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:59
|
||||
msgid "September"
|
||||
msgstr "Szeptember"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:60
|
||||
msgid "October"
|
||||
msgstr "Október"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:61
|
||||
msgid "November"
|
||||
msgstr "November"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:62
|
||||
msgid "December"
|
||||
msgstr "December"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:66
|
||||
msgid "Monday"
|
||||
msgstr "Hétfő"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:67
|
||||
msgid "Tuesday"
|
||||
msgstr "Kedd"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:68
|
||||
msgid "Wednesday"
|
||||
msgstr "Szerda"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:69
|
||||
msgid "Thursday"
|
||||
msgstr "Csütörtök"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:70
|
||||
msgid "Friday"
|
||||
msgstr "Péntek"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:71
|
||||
msgid "Saturday"
|
||||
msgstr "Szombat"
|
||||
|
||||
#: calsocial/calendar_system/gregorian.py:72
|
||||
msgid "Sunday"
|
||||
msgstr "Vasárnap"
|
||||
|
||||
#: calsocial/templates/base.html:29 calsocial/templates/login.html:9
|
||||
#: calsocial/templates/login.html:24 calsocial/templates/welcome.html:7
|
||||
#: calsocial/templates/welcome.html:14
|
||||
msgid "Login"
|
||||
msgstr "Bejelentkezés"
|
||||
|
||||
#: calsocial/templates/base.html:32
|
||||
#, python-format
|
||||
msgid "Logged in as %(username)s"
|
||||
msgstr "Bejelentkezve %(username)s néven"
|
||||
|
||||
#: app/templates/base.html:25
|
||||
msgid "Login"
|
||||
msgstr "Bejelentkezés"
|
||||
#: calsocial/templates/base.html:36
|
||||
msgid "Calendar view"
|
||||
msgstr "Naptár nézet"
|
||||
|
||||
#: app/templates/base.html:27
|
||||
#: calsocial/templates/base.html:37
|
||||
msgid "Notifications"
|
||||
msgstr "Értesítések"
|
||||
|
||||
#: calsocial/templates/base.html:38 calsocial/templates/settings-base.html:8
|
||||
#: calsocial/templates/user-settings.html:5
|
||||
msgid "Settings"
|
||||
msgstr "Beállítások"
|
||||
|
||||
#: calsocial/templates/base.html:39
|
||||
msgid "Logout"
|
||||
msgstr "Kijelentkezés"
|
||||
|
||||
#: app/templates/index.html:4
|
||||
#: calsocial/templates/event-details.html:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This event is organised in the %(timezone)s time zone, in which it "
|
||||
"happens between %(start_time)s and %(end_time)s"
|
||||
msgstr ""
|
||||
"Ez az esemény a(z) %(timezone)s időzónában van szervezve, melyben "
|
||||
"%(start_time)s és %(end_time)s között történik"
|
||||
|
||||
#: calsocial/templates/event-details.html:29
|
||||
msgid "Invited users"
|
||||
msgstr "Meghívott felhasználók"
|
||||
|
||||
#: calsocial/templates/event-details.html:41
|
||||
#: calsocial/templates/event-details.html:47
|
||||
msgid "Invite"
|
||||
msgstr "Meghívás"
|
||||
|
||||
#: calsocial/templates/event-edit.html:23
|
||||
#: calsocial/templates/first-steps.html:25
|
||||
#: calsocial/templates/profile-edit.html:19
|
||||
#: calsocial/templates/user-settings.html:18
|
||||
msgid "Save"
|
||||
msgstr "Mentés"
|
||||
|
||||
#: calsocial/templates/first-steps.html:5
|
||||
msgid "First steps"
|
||||
msgstr "Első lépések"
|
||||
|
||||
#: calsocial/templates/first-steps.html:7
|
||||
msgid "Welcome to Calendar.social!"
|
||||
msgstr "Üdv a Calendar.social-ban!"
|
||||
|
||||
#: calsocial/templates/first-steps.html:10
|
||||
msgid ""
|
||||
"These are the first steps you should make before you can start using the "
|
||||
"site."
|
||||
msgstr ""
|
||||
"Ezek az első lépések melyeket meg kell tenned, mielőtt elkezded használni"
|
||||
" az oldalt."
|
||||
|
||||
#: calsocial/templates/follow-requests.html:4
|
||||
msgid "Follow requests"
|
||||
msgstr "Követési kérések"
|
||||
|
||||
#: calsocial/templates/follow-requests.html:10
|
||||
msgid "Accept"
|
||||
msgstr "Elfogad"
|
||||
|
||||
#: calsocial/templates/follow-requests.html:15
|
||||
msgid "No requests to display."
|
||||
msgstr "Nincs megjeleníthető kérés."
|
||||
|
||||
#: calsocial/templates/follow-requests.html:19
|
||||
msgid "Your profile is not locked."
|
||||
msgstr "A profilod nincs zárolva."
|
||||
|
||||
#: calsocial/templates/follow-requests.html:20
|
||||
msgid "Anyone can follow you without your consent."
|
||||
msgstr "Bárki követhet a beleegyezésed nélkül."
|
||||
|
||||
#: calsocial/templates/index.html:4
|
||||
#, python-format
|
||||
msgid "Welcome to Calendar.social, %(username)s!"
|
||||
msgstr "Üdv a Calendar.social-ben, %(username)s!"
|
||||
|
||||
#: app/templates/registration.html:27
|
||||
#: calsocial/templates/index.html:10
|
||||
msgid "Add event"
|
||||
msgstr "Esemény létrehozása"
|
||||
|
||||
#: calsocial/templates/notifications.html:7
|
||||
msgid "Nothing to show."
|
||||
msgstr "Nincsenek értesítések."
|
||||
|
||||
#: calsocial/templates/profile-details.html:6
|
||||
#: calsocial/templates/profile-details.html:7
|
||||
msgid "locked profile"
|
||||
msgstr "zárolt profil"
|
||||
|
||||
#: calsocial/templates/profile-details.html:13
|
||||
msgid "Follow"
|
||||
msgstr "Követés"
|
||||
|
||||
#: calsocial/templates/profile-details.html:17
|
||||
msgid "Follows"
|
||||
msgstr "Követett felhasználók"
|
||||
|
||||
#: calsocial/templates/profile-details.html:25
|
||||
msgid "Followers"
|
||||
msgstr "Követők"
|
||||
|
||||
#: calsocial/templates/profile-edit.html:5
|
||||
#: calsocial/templates/settings-base.html:7
|
||||
msgid "Edit profile"
|
||||
msgstr "Profil szerkesztése"
|
||||
|
||||
#: calsocial/templates/registration.html:17
|
||||
msgid "Register"
|
||||
msgstr "Regisztráció"
|
||||
|
||||
#: calsocial/templates/welcome.html:18
|
||||
msgid "Or"
|
||||
msgstr "Vagy"
|
||||
|
||||
#: calsocial/templates/welcome.html:20
|
||||
msgid "Register an account"
|
||||
msgstr "Regisztrálj egy fiókot"
|
||||
|
||||
#: calsocial/templates/welcome.html:23
|
||||
msgid "What is Calendar.social?"
|
||||
msgstr "Mi az a Calendar.social?"
|
||||
|
||||
#: calsocial/templates/welcome.html:25
|
||||
msgid ""
|
||||
"Calendar.social is a calendar app based on open protocols and free, open "
|
||||
"source software."
|
||||
msgstr ""
|
||||
"A Calendar.social egy naptár alkalmazás, mely nyílt protokollokat és "
|
||||
"szabad, nyílt forráskódú szoftvereket használ."
|
||||
|
||||
#: calsocial/templates/welcome.html:26
|
||||
msgid "It is decentralised like one of its counterparts, email."
|
||||
msgstr "Decentralizált, mint legismertebb társa, az e-mail."
|
||||
|
||||
#: calsocial/templates/welcome.html:30
|
||||
msgid "Peek inside"
|
||||
msgstr "Less be"
|
||||
|
||||
#: calsocial/templates/welcome.html:36
|
||||
msgid "Built for users in mind"
|
||||
msgstr "A felhasználók igényeire szabva"
|
||||
|
||||
#: calsocial/templates/welcome.html:38
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"From planning your appointments to organising large scale events "
|
||||
"Calendar.social can help with all your scheduling needs."
|
||||
msgstr ""
|
||||
"Találkozók tervezésétől a nagyméretű rendezvények szervezéséig, a "
|
||||
"Calendar.social segít az ütemezésben."
|
||||
|
||||
#: calsocial/templates/welcome.html:47
|
||||
msgid "user"
|
||||
msgid_plural "users"
|
||||
msgstr[0] "felhasználó"
|
||||
|
||||
#: calsocial/templates/welcome.html:53
|
||||
msgid "event"
|
||||
msgid_plural "events"
|
||||
msgstr[0] "esemény"
|
||||
|
||||
#: calsocial/templates/welcome.html:60
|
||||
msgid "Built for people"
|
||||
msgstr "Embereknek készítve"
|
||||
|
||||
#: calsocial/templates/welcome.html:62
|
||||
msgid "Calendar.social is not a commercial network."
|
||||
msgstr "A Calendar.social nem egy kereskedelmi hálózat."
|
||||
|
||||
#: calsocial/templates/welcome.html:63
|
||||
msgid "No advertising, no data mining, no walled gardens."
|
||||
msgstr "Nincs reklám, nincs adatbányászat, nincsenek korlátok."
|
||||
|
||||
#: calsocial/templates/welcome.html:64
|
||||
msgid "There is no central authority."
|
||||
msgstr "Nincs központi autoritás."
|
||||
|
||||
#: calsocial/templates/welcome.html:69
|
||||
msgid "Administered by"
|
||||
msgstr "Üzemelteti"
|
||||
|
||||
|
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
|
||||
for key, value in orig_attrs.items():
|
||||
setattr(ctx, key, value)
|
||||
|
||||
|
||||
class RoutedMixin:
|
||||
"""Mixin to lazily register class methods as routes
|
||||
|
||||
Works both for `Flask` and `Blueprint` objects.
|
||||
|
||||
Example::
|
||||
|
||||
class MyBlueprint(Blueprint, RoutedMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
do_whatever_you_like()
|
||||
|
||||
RoutedMixin.register_routes(self)
|
||||
|
||||
@RoutedMixin.route('/')
|
||||
def index(self):
|
||||
return 'Hello, World!'
|
||||
"""
|
||||
|
||||
def register_routes(self):
|
||||
"""Register all routes that were marked with :meth:`route`
|
||||
"""
|
||||
|
||||
for attr_name in self.__dir__():
|
||||
attr = getattr(self, attr_name)
|
||||
|
||||
if not callable(attr):
|
||||
continue
|
||||
|
||||
args, kwargs = getattr(attr, 'routing', (None, None))
|
||||
|
||||
if args is None:
|
||||
continue
|
||||
|
||||
self.route(*args, **kwargs)(attr)
|
||||
|
||||
@staticmethod
|
||||
def route(*args, **kwargs):
|
||||
"""Mark a function as a future route
|
||||
|
||||
Such functions will be iterated over when the application is initialised. ``*args`` and
|
||||
``**kwargs`` will be passed verbatim to `Flask.route()`.
|
||||
"""
|
||||
|
||||
def decorator(func): # pylint: disable=missing-docstring
|
||||
setattr(func, 'routing', (args, kwargs))
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
62
tests/conftest.py
Normal file
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 calsocial
|
||||
from calsocial.models import db, User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
|
||||
calsocial.app.config['TESTING'] = True
|
||||
calsocial.app.config['WTF_CSRF_ENABLED'] = False
|
||||
client = calsocial.app.test_client()
|
||||
|
||||
with calsocial.app.app_context():
|
||||
db.create_all()
|
||||
|
||||
yield client
|
||||
|
||||
with calsocial.app.app_context():
|
||||
db.drop_all()
|
||||
|
||||
|
||||
def test_index_no_login(client):
|
||||
"""Test the main page without logging in
|
||||
"""
|
||||
|
||||
page = client.get('/')
|
||||
assert b'Welcome to Calendar.social' in page.data
|
||||
assert b'Peek inside' in page.data
|
||||
|
||||
|
||||
def test_register_page(client):
|
||||
"""Test the registration page
|
||||
def test_instance_adin_unset(database):
|
||||
"""Test the instance admin feature if the admin is not set
|
||||
"""
|
||||
|
||||
page = client.get('/register')
|
||||
assert b'Register</button>' in page.data
|
||||
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is None\)'):
|
||||
assert current_app.instance_admin is None
|
||||
|
||||
def test_register_post_empty(client):
|
||||
"""Test sending empty registration data
|
||||
|
||||
def test_instance_admin_bad_value(database):
|
||||
"""Test the instance admin feature if the value is invalid
|
||||
"""
|
||||
|
||||
page = client.post('/register', data={})
|
||||
assert b'This field is required' in page.data
|
||||
from calsocial.models import AppState
|
||||
|
||||
def test_register_invalid_email(client):
|
||||
"""Test sending an invalid email address
|
||||
AppState['instance_admin'] = 'value'
|
||||
|
||||
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is value\)'):
|
||||
assert current_app.instance_admin is None
|
||||
|
||||
|
||||
def test_instance_admin_doesnot_exist(database):
|
||||
"""Test the instance admin feature if the admin user does not exist
|
||||
"""
|
||||
|
||||
page = client.post('/register', data={
|
||||
'username': 'test',
|
||||
'email': 'test',
|
||||
'password': 'password',
|
||||
'password_retype': 'password',
|
||||
})
|
||||
assert b'Invalid email address' in page.data
|
||||
from calsocial.models import AppState
|
||||
|
||||
def test_register_password_mismatch(client):
|
||||
"""Test sending different password for registration
|
||||
AppState['instance_admin'] = '0'
|
||||
|
||||
with pytest.warns(UserWarning, match=r'Instance admin is not set correctly \(value is 0\)'):
|
||||
assert current_app.instance_admin is None
|
||||
|
||||
|
||||
def test_instance_admin(database):
|
||||
"""Test the instance admin feature if the admin user does not exist
|
||||
"""
|
||||
|
||||
page = client.post('/register', data={
|
||||
'username': 'test',
|
||||
'email': 'test@example.com',
|
||||
'password': 'password',
|
||||
'password_retype': 'something',
|
||||
})
|
||||
assert b'The two passwords must match' in page.data
|
||||
from calsocial.models import db, AppState, User
|
||||
|
||||
def test_register(client):
|
||||
"""Test user registration
|
||||
"""
|
||||
user = User(username='admin')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
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/'
|
||||
AppState['instance_admin'] = user.id
|
||||
|
||||
with calsocial.app.app_context():
|
||||
user = User.query.one()
|
||||
|
||||
assert user.username == 'test'
|
||||
assert user.email == 'test@example.com'
|
||||
|
||||
def test_register_existing_username(client):
|
||||
"""Test registering an existing username
|
||||
"""
|
||||
|
||||
with calsocial.app.app_context():
|
||||
user = User(username='test', email='test@example.com')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
page = client.post('/register', data={
|
||||
'username': 'test',
|
||||
'email': 'test2@example.com',
|
||||
'password': 'password',
|
||||
'password_retype': 'password',
|
||||
})
|
||||
assert b'This username is not available' in page.data
|
||||
|
||||
def test_register_existing_email(client):
|
||||
"""Test registering an existing email address
|
||||
"""
|
||||
|
||||
with calsocial.app.app_context():
|
||||
user = User(username='test', email='test@example.com')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
page = client.post('/register', data={
|
||||
'username': 'tester',
|
||||
'email': 'test@example.com',
|
||||
'password': 'password',
|
||||
'password_retype': 'password',
|
||||
})
|
||||
assert b'This email address can not be used' in page.data
|
||||
assert current_app.instance_admin == user
|
||||
|
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
|
Loading…
Reference in New Issue
Block a user