1 Commits

Author SHA1 Message Date
86f86996f6 Add Drone configuration 2018-07-11 11:08:16 +02:00
42 changed files with 561 additions and 2501 deletions

17
.drone.yml Normal file
View File

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

5
.gitignore vendored
View File

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

View File

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

107
Pipfile.lock generated
View File

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

View File

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

View File

@@ -1,234 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Main module for the Calendar.social app
"""
from flask import Blueprint, abort, current_app, flash, redirect, render_template, session, url_for
from flask_babelex import gettext as _
from flask_security import current_user, login_required
from sqlalchemy.orm.exc import NoResultFound
from calsocial.utils import RoutedMixin
class AccountBlueprint(Blueprint, RoutedMixin):
"""Blueprint for account management
"""
def __init__(self):
Blueprint.__init__(self, 'account', __name__)
self.app = None
RoutedMixin.register_routes(self)
def init_app(self, app, url_prefix=None):
"""Initialise the blueprint, registering it with ``app``.
"""
self.app = app
app.register_blueprint(self, url_prefix=url_prefix)
@staticmethod
@RoutedMixin.route('/register', methods=['POST', 'GET'])
def register_account():
"""View for user registration
If the ``REGISTRATION_FAILED`` configuration value is set to ``True`` it displays the
registration disabled template. Otherwise, it performs user registration.
"""
if not current_app.config['REGISTRATION_ENABLED']:
return render_template('registration-disabled.html')
from .forms import RegistrationForm
from .models import db, User
form = RegistrationForm()
if form.validate_on_submit():
# TODO: This might become False later, if we want registrations to be confirmed via
# e-mail
user = User(active=True)
form.populate_obj(user)
db.session.add(user)
db.session.commit()
return redirect(url_for('hello'))
return render_template('account/registration.html', form=form)
@staticmethod
@RoutedMixin.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
"""View for user settings
"""
from .forms import SettingsForm
from .models import db
form = SettingsForm(current_user)
if form.validate_on_submit():
form.populate_obj(current_user)
db.session.commit()
return redirect(url_for('hello'))
return render_template('account/user-settings.html', form=form)
@staticmethod
@RoutedMixin.route('/notifications')
def notifications():
"""View to list the notifications for the current user
"""
from .models import Notification
if current_user.is_authenticated:
notifs = Notification.query.filter(Notification.profile == current_user.profile)
else:
notifs = []
return render_template('account/notifications.html', notifs=notifs)
@staticmethod
@RoutedMixin.route('/first-steps', methods=['GET', 'POST'])
@login_required
def first_steps():
"""View to set up a new registrants profile
"""
from .forms import FirstStepsForm
from .models import db, Profile
if current_user.profile:
return redirect(url_for('hello'))
form = FirstStepsForm()
if form.validate_on_submit():
profile = Profile(user=current_user, display_name=form.display_name.data)
db.session.add(profile)
current_user.settings['timezone'] = str(form.time_zone.data)
db.session.commit()
return redirect(url_for('hello'))
return render_template('account/first-steps.html', form=form)
@staticmethod
@RoutedMixin.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
"""View for editing ones profile
"""
from .forms import ProfileForm
from .models import db
form = ProfileForm(current_user.profile)
if form.validate_on_submit():
form.populate_obj(current_user.profile)
db.session.add(current_user.profile)
db.session.commit()
return redirect(url_for('account.edit_profile'))
return render_template('account/profile-edit.html', form=form)
@staticmethod
@RoutedMixin.route('/follow-requests')
@login_required
def follow_requests():
"""View for listing follow requests
"""
from .models import UserFollow
requests = UserFollow.query \
.filter(UserFollow.followed == current_user.profile) \
.filter(UserFollow.accepted_at.is_(None))
return render_template('account/follow-requests.html', requests=requests)
@staticmethod
@RoutedMixin.route('/follow-request/<int:follower_id>/accept')
@login_required
def accept_follow(follower_id):
"""View for accepting a follow request
"""
from .models import db, UserFollow
try:
req = UserFollow.query \
.filter(UserFollow.followed == current_user.profile) \
.filter(UserFollow.follower_id == follower_id) \
.one()
except NoResultFound:
abort(404)
if req.accepted_at is None:
req.accept()
db.session.add(req)
db.session.commit()
return redirect(url_for('account.follow_requests'))
@staticmethod
@RoutedMixin.route('/sessions')
@login_required
def active_sessions():
"""View the list of active sessions
"""
sessions = [current_app.session_interface.load_session(sid)
for sid in current_user.active_sessions]
return render_template('account/active-sessions.html', sessions=sessions)
@staticmethod
@RoutedMixin.route('/sessions/invalidate/<string:sid>')
@login_required
def invalidate_session(sid):
"""View to invalidate a session
"""
sess = current_app.session_interface.load_session(sid)
if not sess or sess.user != current_user:
abort(404)
if sess.sid == session.sid:
flash(_('Cant invalidate your current session'))
else:
current_app.session_interface.delete_session(sid)
current_user.active_sessions = [sess_id
for sess_id in current_user.active_sessions
if sess_id != sid]
return redirect(url_for('account.active_sessions'))

View File

@@ -1,159 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Caching functionality for Calendar.social
"""
from datetime import timedelta
import pickle
from uuid import uuid4
from flask import has_request_context, request as flask_request, session as flask_session
from flask.sessions import SessionInterface, SessionMixin
from flask_caching import Cache
from werkzeug.datastructures import CallbackDict
cache = Cache() # pylint: disable=invalid-name
class CachedSession(CallbackDict, SessionMixin): # pylint: disable=too-many-ancestors
"""Object for session data saved in the cache
"""
def __init__(self, initial=None, sid=None, new=False):
self.__modifying = False
def on_update(self):
"""Function to call when session data is updated
"""
if self.__modifying:
return
self.__modifying = True
if has_request_context():
self['ip'] = flask_request.remote_addr
self.modified = True
self.__modifying = False
CallbackDict.__init__(self, initial, on_update)
self.sid = sid
self.new = new
self.modified = False
@property
def user(self):
"""The user this session belongs to
"""
from calsocial.models import User
if 'user_id' not in self:
return None
return User.query.get(self['user_id'])
class CachedSessionInterface(SessionInterface):
"""A session interface that loads/saves session data from the cache
"""
serializer = pickle
session_class = CachedSession
global_cache = cache
def __init__(self, prefix='session:'):
self.cache = cache
self.prefix = prefix
@staticmethod
def generate_sid():
"""Generade a new session ID
"""
return str(uuid4())
@staticmethod
def get_cache_expiration_time(app, sess):
"""Get the expiration time of the cache entry
"""
if sess.permanent:
return app.permanent_session_lifetime
return timedelta(days=1)
def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = self.generate_sid()
return self.session_class(sid=sid, new=True)
session = self.load_session(sid)
if session is None:
return self.session_class(sid=sid, new=True)
return session
def load_session(self, sid):
"""Load a specific session from the cache
"""
val = self.cache.get(self.prefix + sid)
if val is None:
return None
data = self.serializer.loads(val)
return self.session_class(data, sid=sid)
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
if not session:
self.cache.delete(self.prefix + session.sid)
if session.modified:
response.delete_cookie(app.session_cookie_name, domain=domain)
return
cache_exp = self.get_cache_expiration_time(app, session)
cookie_exp = self.get_expiration_time(app, session)
val = self.serializer.dumps(dict(session))
self.cache.set(self.prefix + session.sid, val, int(cache_exp.total_seconds()))
response.set_cookie(app.session_cookie_name,
session.sid,
expires=cookie_exp,
httponly=True,
domain=domain)
def delete_session(self, sid):
"""Delete the session with ``sid`` as its session ID
"""
if has_request_context() and flask_session.sid == sid:
raise ValueError('Will not delete the current session')
cache.delete(self.prefix + sid)

View File

@@ -83,23 +83,17 @@ class GregorianCalendar(CalendarSystem):
def days(self): def days(self):
day_list = [] day_list = []
month_first = self.timestamp.replace(day=1) start_day = self.timestamp.replace(day=1)
if self.timestamp.month == 12: while start_day.weekday() > self.START_DAY:
month_last = month_first.replace(day=31) start_day -= timedelta(days=1)
else:
month_last = month_first.replace(month=month_first.month + 1) - timedelta(days=1)
pad_before = (7 - self.START_DAY + month_first.weekday()) % 7 day_list.append(start_day)
pad_after = (6 - month_last.weekday() + self.START_DAY) % 7 current_day = start_day
first_display = month_first - timedelta(days=pad_before) while current_day.weekday() < self.END_DAY and current_day.month <= self.timestamp.month:
last_display = month_last + timedelta(days=pad_after) current_day += timedelta(days=1)
current = first_display day_list.append(current_day)
while current <= last_display:
day_list.append(current)
current += timedelta(days=1)
return day_list return day_list
@@ -205,29 +199,21 @@ class GregorianCalendar(CalendarSystem):
"""Returns all events for a given day """Returns all events for a given day
""" """
from ..models import Event, EventVisibility, Invitation, Profile, Response from ..models import Event, Profile
events = Event.query events = Event.query
if user: if user:
events = events.outerjoin(Invitation) \ events = events.join(Profile) \
.outerjoin(Response) \
.join(Profile, Event.profile) \
.filter(Profile.user == user) .filter(Profile.user == user)
start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0) start_timestamp = date.replace(hour=0, minute=0, second=0, microsecond=0)
end_timestamp = start_timestamp + timedelta(days=1) end_timestamp = start_timestamp + timedelta(days=1)
events = events.filter((Event.start_time <= end_timestamp) & events = events.filter(((Event.start_time >= start_timestamp) &
(Event.end_time >= start_timestamp)) \ (Event.start_time < end_timestamp)) |
((Event.end_time >= start_timestamp) &
(Event.end_time < end_timestamp))) \
.order_by('start_time', 'end_time') .order_by('start_time', 'end_time')
if user is None:
events = events.filter(Event.visibility == EventVisibility.public)
else:
events = events.filter((Event.visibility == EventVisibility.public) |
(Event.profile == user.profile) |
(Invitation.invitee == user.profile) |
(Response.profile == user.profile))
return events return events

View File

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

View File

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

View File

@@ -27,7 +27,6 @@ from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy_utils.types.choice import ChoiceType from sqlalchemy_utils.types.choice import ChoiceType
from .cache import cache
from .utils import force_locale from .utils import force_locale
db = SQLAlchemy() db = SQLAlchemy()
@@ -104,23 +103,6 @@ class ResponseType(Enum):
return Enum.__eq__(self, other) return Enum.__eq__(self, other)
class EventVisibility(Enum):
"""Enumeration for event visibility
"""
#: The event is private, only attendees and people invited can see the details
private = 0
#: The event is public, anyone can see the details
public = 5
EVENT_VISIBILITY_TRANSLATIONS = {
EventVisibility.private: _('Visible only to attendees'),
EventVisibility.public: _('Visible to everyone'),
}
class SettingsProxy: class SettingsProxy:
"""Proxy object to get settings for a user """Proxy object to get settings for a user
""" """
@@ -220,24 +202,6 @@ class User(db.Model, UserMixin):
return current_app.timezone return current_app.timezone
@property
def session_list_key(self):
"""The cache key of this users session list
"""
return f'open_sessions:{self.id}'
@property
def active_sessions(self):
"""The list of active sessions of this user
"""
return cache.get(self.session_list_key) or []
@active_sessions.setter
def active_sessions(self, value):
cache.set(self.session_list_key, list(value))
def __repr__(self): def __repr__(self):
return f'<User {self.id}({self.username})>' return f'<User {self.id}({self.username})>'
@@ -282,9 +246,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
#: The display name #: The display name
display_name = db.Column(db.Unicode(length=80), nullable=False) display_name = db.Column(db.Unicode(length=80), nullable=False)
#: If locked, a profile cannot be followed without the owners consent
locked = db.Column(db.Boolean(), default=False)
@property @property
def fqn(self): def fqn(self):
"""The fully qualified name of the profile """The fully qualified name of the profile
@@ -304,7 +265,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
domain = '' domain = ''
else: else:
username = self.username username = self.username
domain = f'@{self.domain}' domain = '@' + self.domain
return f'<Profile {self.id}(@{username}{domain})>' return f'<Profile {self.id}(@{username}{domain})>'
@@ -329,8 +290,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \ return Profile.query \
.join(UserFollow.followed) \ .join(UserFollow.followed) \
.filter(UserFollow.follower == self) \ .filter(UserFollow.follower == self)
.filter(UserFollow.accepted_at.isnot(None))
@property @property
def follower_list(self): def follower_list(self):
@@ -343,8 +303,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return Profile.query \ return Profile.query \
.join(UserFollow.follower) \ .join(UserFollow.follower) \
.filter(UserFollow.followed == self) \ .filter(UserFollow.followed == self)
.filter(UserFollow.accepted_at.isnot(None))
@property @property
def url(self): def url(self):
@@ -358,48 +317,6 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
return NotImplemented return NotImplemented
def follow(self, follower):
"""Make ``follower`` follow this profile
"""
if not isinstance(follower, Profile):
raise TypeError('Folloer must be a Profile object')
timestamp = None if self.locked else datetime.utcnow()
user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp)
db.session.add(user_follow)
notification = self.notify(follower, self, NotificationAction.follow)
db.session.add(notification)
return user_follow
def notify(self, actor, item, action):
"""Notify this profile about ``action`` on ``item`` by ``actor``
:param actor: the actor who generated the notification
:type actor: Profile
:param item: the item ``action`` was performed on
:type item: any
:param action: the type of the action
:type action: NotificationAction, str
:raises TypeError: if ``actor`` is not a `Profile` object
:returns: the generated notification. It is already added to the database session, but
not committed
:rtype: Notification
"""
if not isinstance(actor, Profile):
raise TypeError('actor must be a Profile instance')
if isinstance(action, str):
action = NotificationAction[action]
notification = Notification(profile=self, actor=actor, item=item, action=action)
return notification
class Event(db.Model): class Event(db.Model):
"""Database model for events """Database model for events
@@ -435,9 +352,6 @@ class Event(db.Model):
#: The description of the event #: The description of the event
description = db.Column(db.UnicodeText()) description = db.Column(db.UnicodeText())
#: The visibility of the event
visibility = db.Column(db.Enum(EventVisibility), nullable=False)
def __as_tz(self, timestamp, as_timezone=None): def __as_tz(self, timestamp, as_timezone=None):
from pytz import timezone, utc from pytz import timezone, utc
@@ -486,20 +400,6 @@ class Event(db.Model):
return url_for('event_details', event_uuid=self.event_uuid) return url_for('event_details', event_uuid=self.event_uuid)
def invite(self, inviter, invited):
"""Invite ``invited`` to the event
The invitation will arrive from ``inviter``.
"""
invite = Invitation(event=self, sender=inviter, invitee=invited)
db.session.add(invite)
notification = invited.notify(inviter, self, NotificationAction.invite)
db.session.add(notification)
return invite
class UserSetting(db.Model): # pylint: disable=too-few-public-methods class UserSetting(db.Model): # pylint: disable=too-few-public-methods
"""Database model for user settings """Database model for user settings
@@ -643,12 +543,6 @@ class UserFollow(db.Model): # pylint: disable=too-few-public-methods
#: The timestamp when the follow was accepted #: The timestamp when the follow was accepted
accepted_at = db.Column(db.DateTime(), nullable=True) accepted_at = db.Column(db.DateTime(), nullable=True)
def accept(self):
"""Accept this follow request
"""
self.accepted_at = datetime.utcnow()
class Notification(db.Model): class Notification(db.Model):
"""Database model for notifications """Database model for notifications

View File

@@ -17,7 +17,7 @@
"""Security related things for Calendar.social """Security related things for Calendar.social
""" """
from flask import current_app, session from flask import current_app
from flask_login.signals import user_logged_in, user_logged_out from flask_login.signals import user_logged_in, user_logged_out
from flask_security import Security, AnonymousUser as BaseAnonymousUser from flask_security import Security, AnonymousUser as BaseAnonymousUser
@@ -45,8 +45,6 @@ def login_handler(app, user): # pylint: disable=unused-argument
AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS) AuditLog.log(user, AuditLog.TYPE_LOGIN_SUCCESS)
user.active_sessions += [session.sid]
@user_logged_out.connect @user_logged_out.connect
def logout_handler(app, user): # pylint: disable=unused-argument def logout_handler(app, user): # pylint: disable=unused-argument

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
{% macro field(field, inline=false) %}
<div class="{% if inline %}inline {% endif %}field">
{% if field.errors %}
{% for error in field.errors %}
{{ error }}
{% endfor %}
<br>
{% endif %}
{% if field.widget.input_type != 'checkbox' %}
{{ field.label }}
{% endif %}
{{ field }}
{% if field.widget.input_type == 'checkbox' %}
{{ field.label }}<br>
{% endif %}
{% if field.description %}
{{ field.description }}
{% endif %}
</div>
{% endmacro %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,7 @@
{% block content %} {% block content %}
<h1> <h1>
{% if profile.locked %} {{ profile.name }}
<i class="fa fa-lock" aria-hidden="true" title="{% trans %}locked profile{% endtrans %}"></i>
<span class="sr-only">{% trans %}locked profile{% endtrans %}</span>
{% endif %}
{{ profile.display_name }}
<small>@{{ profile.user.username}}</small> <small>@{{ profile.user.username}}</small>
</h1> </h1>
{% if profile.user != current_user %} {% if profile.user != current_user %}

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,54 +68,3 @@ def force_locale(locale):
babel.locale_selector_func = orig_locale_selector_func babel.locale_selector_func = orig_locale_selector_func
for key, value in orig_attrs.items(): for key, value in orig_attrs.items():
setattr(ctx, key, value) setattr(ctx, key, value)
class RoutedMixin:
"""Mixin to lazily register class methods as routes
Works both for `Flask` and `Blueprint` objects.
Example::
class MyBlueprint(Blueprint, RoutedMixin):
def __init__(self, *args, **kwargs):
do_whatever_you_like()
RoutedMixin.register_routes(self)
@RoutedMixin.route('/')
def index(self):
return 'Hello, World!'
"""
def register_routes(self):
"""Register all routes that were marked with :meth:`route`
"""
for attr_name in self.__dir__():
attr = getattr(self, attr_name)
if not callable(attr):
continue
args, kwargs = getattr(attr, 'routing', (None, None))
if args is None:
continue
self.route(*args, **kwargs)(attr)
@staticmethod
def route(*args, **kwargs):
"""Mark a function as a future route
Such functions will be iterated over when the application is initialised. ``*args`` and
``**kwargs`` will be passed verbatim to `Flask.route()`.
"""
def decorator(func): # pylint: disable=missing-docstring
setattr(func, 'routing', (args, kwargs))
return func
return decorator

View File

@@ -1,93 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Helper functions and fixtures for testing
"""
from contextlib import contextmanager
import pytest
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
@pytest.fixture
def client():
"""Fixture that provides a Flask test client
"""
configure_app()
client = calsocial.app.test_client()
with calsocial.app.app_context():
db.create_all()
yield client
with calsocial.app.app_context():
db.drop_all()
def login(client, username, password, no_redirect=False):
"""Login with the specified username and password
"""
return client.post('/login',
data={'email': username, 'password': password},
follow_redirects=not no_redirect)
@pytest.fixture
def database():
"""Fixture to provide all database tables in an active application context
"""
configure_app()
with calsocial.app.app_context():
db.create_all()
yield db
db.drop_all()
@contextmanager
def alter_config(app, **kwargs):
saved = {}
for key, value in kwargs.items():
if key in app.config:
saved[key] = app.config[key]
app.config[key] = value
yield
for key, value in kwargs.items():
if key in saved:
app.config[key] = saved[key]
else:
del app.config[key]

View File

@@ -1,23 +1,23 @@
# Calendar.social import pytest
# 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 client
@pytest.fixture
def client():
calsocial.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
calsocial.app.config['TESTING'] = True
calsocial.app.config['WTF_CSRF_ENABLED'] = False
client = calsocial.app.test_client()
with calsocial.app.app_context():
db.create_all()
yield client
with calsocial.app.app_context():
db.drop_all()
def test_index_no_login(client): def test_index_no_login(client):
@@ -25,4 +25,97 @@ def test_index_no_login(client):
""" """
page = client.get('/') page = client.get('/')
assert b'Peek inside' in page.data assert b'Welcome to Calendar.social' in page.data
def test_register_page(client):
"""Test the registration page
"""
page = client.get('/register')
assert b'Register</button>' in page.data
def test_register_post_empty(client):
"""Test sending empty registration data
"""
page = client.post('/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('/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('/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('/register', data={
'username': 'test',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
print(page.data)
assert page.status_code == 302
assert page.location == 'http://localhost/'
with calsocial.app.app_context():
user = User.query.one()
assert user.username == 'test'
assert user.email == 'test@example.com'
def test_register_existing_username(client):
"""Test registering an existing username
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'test',
'email': 'test2@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This username is not available' in page.data
def test_register_existing_email(client):
"""Test registering an existing email address
"""
with calsocial.app.app_context():
user = User(username='test', email='test@example.com')
db.session.add(user)
db.session.commit()
page = client.post('/register', data={
'username': 'tester',
'email': 'test@example.com',
'password': 'password',
'password_retype': 'password',
})
assert b'This email address can not be used' in page.data

View File

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

View File

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

View File

@@ -1,121 +0,0 @@
# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""General tests for Calendar.social
"""
import calsocial
from calsocial.models import db, User
from helpers import alter_config, client
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