Compare commits
22 Commits
drone-ci
...
model-upda
Author | SHA1 | Date | |
---|---|---|---|
06c7b2ea52 | |||
3308be40ee | |||
9b01431641 | |||
2b1378310a | |||
5639c3f578 | |||
61f10f951c | |||
496b5b6c04 | |||
dc0b2954c1 | |||
36c2f0fd77 | |||
27c78ff36f | |||
37e08fed22 | |||
a0fba3f2af | |||
48a19a2296 | |||
5d886a7853 | |||
5550e5ecf3 | |||
0a3cfafef3 | |||
8e3bcd8ede | |||
48ffb0d472 | |||
c3348d3212 | |||
1a69928241 | |||
7b935afdad | |||
303dd3d082 |
17
.drone.yml
17
.drone.yml
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
/calsocial/local.db
|
/calsocial/local.db
|
||||||
/messages.pot
|
/messages.pot
|
||||||
/app/translations/*/LC_MESSAGES/*.mo
|
/calsocial/translations/*/LC_MESSAGES/*.mo
|
||||||
/.pytest_cache/
|
/.pytest_cache/
|
||||||
|
@@ -170,7 +170,7 @@ class CalendarSocialApp(Flask):
|
|||||||
|
|
||||||
calendar = GregorianCalendar(timestamp.timestamp())
|
calendar = GregorianCalendar(timestamp.timestamp())
|
||||||
|
|
||||||
return render_template('index.html', calendar=calendar)
|
return render_template('index.html', calendar=calendar, user_only=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@route('/register', methods=['POST', 'GET'])
|
@route('/register', methods=['POST', 'GET'])
|
||||||
@@ -217,7 +217,7 @@ class CalendarSocialApp(Flask):
|
|||||||
form = EventForm()
|
form = EventForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
event = Event(user=current_user)
|
event = Event(profile=current_user.profile)
|
||||||
form.populate_obj(event)
|
form.populate_obj(event)
|
||||||
|
|
||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
@@ -255,7 +255,7 @@ class CalendarSocialApp(Flask):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .forms import InviteForm
|
from .forms import InviteForm
|
||||||
from .models import db, Event, Invitation, Notification, NotificationAction
|
from .models import db, Event
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = Event.query.filter(Event.event_uuid == event_uuid).one()
|
event = Event.query.filter(Event.event_uuid == event_uuid).one()
|
||||||
@@ -267,15 +267,7 @@ class CalendarSocialApp(Flask):
|
|||||||
form = InviteForm(event)
|
form = InviteForm(event)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
invite = Invitation(event=event, sender=current_user.profile)
|
event.invite(current_user.profile, invitee=form.invitee.data)
|
||||||
form.populate_obj(invite)
|
|
||||||
db.session.add(invite)
|
|
||||||
|
|
||||||
notification = Notification(profile=form.invitee.data,
|
|
||||||
actor=current_user.profile,
|
|
||||||
item=event,
|
|
||||||
action=NotificationAction.invite)
|
|
||||||
db.session.add(notification)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -300,11 +292,12 @@ class CalendarSocialApp(Flask):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@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, UserFollow, Notification, NotificationAction
|
from .models import db, Profile, User
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile = Profile.query.join(User).filter(User.username == username).one()
|
profile = Profile.query.join(User).filter(User.username == username).one()
|
||||||
@@ -312,16 +305,7 @@ class CalendarSocialApp(Flask):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if profile.user != current_user:
|
if profile.user != current_user:
|
||||||
follow = UserFollow(follower=current_user.profile,
|
profile.follow(follower=current_user.profile)
|
||||||
followed=profile,
|
|
||||||
accepted_at=datetime.utcnow())
|
|
||||||
db.session.add(follow)
|
|
||||||
|
|
||||||
notification = Notification(profile=profile,
|
|
||||||
actor=current_user.profile,
|
|
||||||
item=profile,
|
|
||||||
action=NotificationAction.follow)
|
|
||||||
db.session.add(notification)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -399,5 +383,83 @@ class CalendarSocialApp(Flask):
|
|||||||
|
|
||||||
return render_template('first-steps.html', form=form)
|
return render_template('first-steps.html', form=form)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@route('/edit-profile', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_profile():
|
||||||
|
"""View for editing one’s profile
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .forms import ProfileForm
|
||||||
|
from .models import db
|
||||||
|
|
||||||
|
form = ProfileForm(current_user.profile)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.populate_obj(current_user.profile)
|
||||||
|
db.session.add(current_user.profile)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('edit_profile'))
|
||||||
|
|
||||||
|
return render_template('profile-edit.html', form=form)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@route('/all-events')
|
||||||
|
def all_events():
|
||||||
|
"""View for listing all available events
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .calendar_system.gregorian import GregorianCalendar
|
||||||
|
|
||||||
|
try:
|
||||||
|
timestamp = datetime.fromtimestamp(float(request.args.get('date')))
|
||||||
|
except TypeError:
|
||||||
|
timestamp = datetime.utcnow()
|
||||||
|
|
||||||
|
calendar = GregorianCalendar(timestamp.timestamp())
|
||||||
|
|
||||||
|
return render_template('index.html', calendar=calendar, user_only=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@route('/follow-requests')
|
||||||
|
@login_required
|
||||||
|
def follow_requests():
|
||||||
|
"""View for listing follow requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .models import UserFollow
|
||||||
|
|
||||||
|
requests = UserFollow.query \
|
||||||
|
.filter(UserFollow.followed == current_user.profile) \
|
||||||
|
.filter(UserFollow.accepted_at.is_(None))
|
||||||
|
|
||||||
|
return render_template('follow-requests.html', requests=requests)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@route('/follow-request/<int:follower_id>/accept')
|
||||||
|
@login_required
|
||||||
|
def accept_follow(follower_id):
|
||||||
|
"""View for accepting a follow request
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .models import db, UserFollow
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = UserFollow.query \
|
||||||
|
.filter(UserFollow.followed == current_user.profile) \
|
||||||
|
.filter(UserFollow.follower_id == follower_id) \
|
||||||
|
.one()
|
||||||
|
except NoResultFound:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if req.accepted_at is None:
|
||||||
|
req.accept()
|
||||||
|
|
||||||
|
db.session.add(req)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('follow_requests'))
|
||||||
|
|
||||||
|
|
||||||
app = CalendarSocialApp(__name__)
|
app = CalendarSocialApp(__name__)
|
||||||
|
@@ -330,8 +330,24 @@ 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
|
||||||
|
@@ -103,6 +103,24 @@ class ResponseType(Enum):
|
|||||||
return Enum.__eq__(self, other)
|
return Enum.__eq__(self, other)
|
||||||
|
|
||||||
|
|
||||||
|
class EventAvailability(Enum):
|
||||||
|
free = 0
|
||||||
|
busy = 1
|
||||||
|
|
||||||
|
|
||||||
|
class UserAvailability(EventAvailability):
|
||||||
|
tentative = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseVisibility(Enum):
|
||||||
|
private = 0
|
||||||
|
organisers = 1
|
||||||
|
attendees = 2
|
||||||
|
followers = 3
|
||||||
|
friends = 4
|
||||||
|
public = 5
|
||||||
|
|
||||||
|
|
||||||
class SettingsProxy:
|
class SettingsProxy:
|
||||||
"""Proxy object to get settings for a user
|
"""Proxy object to get settings for a user
|
||||||
"""
|
"""
|
||||||
@@ -246,6 +264,9 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
#: The display name
|
#: The display name
|
||||||
display_name = db.Column(db.Unicode(length=80), nullable=False)
|
display_name = db.Column(db.Unicode(length=80), nullable=False)
|
||||||
|
|
||||||
|
#: If locked, a profile cannot be followed without the owner’s consent
|
||||||
|
locked = db.Column(db.Boolean(), default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fqn(self):
|
def fqn(self):
|
||||||
"""The fully qualified name of the profile
|
"""The fully qualified name of the profile
|
||||||
@@ -265,7 +286,7 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
domain = ''
|
domain = ''
|
||||||
else:
|
else:
|
||||||
username = self.username
|
username = self.username
|
||||||
domain = '@' + self.domain
|
domain = f'@{self.domain}'
|
||||||
|
|
||||||
return f'<Profile {self.id}(@{username}{domain})>'
|
return f'<Profile {self.id}(@{username}{domain})>'
|
||||||
|
|
||||||
@@ -290,7 +311,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
return Profile.query \
|
return Profile.query \
|
||||||
.join(UserFollow.followed) \
|
.join(UserFollow.followed) \
|
||||||
.filter(UserFollow.follower == self)
|
.filter(UserFollow.follower == self) \
|
||||||
|
.filter(UserFollow.accepted_at.isnot(None))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def follower_list(self):
|
def follower_list(self):
|
||||||
@@ -303,7 +325,8 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
return Profile.query \
|
return Profile.query \
|
||||||
.join(UserFollow.follower) \
|
.join(UserFollow.follower) \
|
||||||
.filter(UserFollow.followed == self)
|
.filter(UserFollow.followed == self) \
|
||||||
|
.filter(UserFollow.accepted_at.isnot(None))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
@@ -317,6 +340,48 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
def follow(self, follower):
|
||||||
|
"""Make ``follower`` follow this profile
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(follower, Profile):
|
||||||
|
raise TypeError('Folloer must be a Profile object')
|
||||||
|
|
||||||
|
timestamp = None if self.locked else datetime.utcnow()
|
||||||
|
|
||||||
|
user_follow = UserFollow(follower=follower, followed=self, accepted_at=timestamp)
|
||||||
|
db.session.add(user_follow)
|
||||||
|
notification = self.notify(follower, self, NotificationAction.follow)
|
||||||
|
|
||||||
|
db.session.add(notification)
|
||||||
|
|
||||||
|
return user_follow
|
||||||
|
|
||||||
|
def notify(self, actor, item, action):
|
||||||
|
"""Notify this profile about ``action`` on ``item`` by ``actor``
|
||||||
|
|
||||||
|
:param actor: the actor who generated the notification
|
||||||
|
:type actor: Profile
|
||||||
|
:param item: the item ``action`` was performed on
|
||||||
|
:type item: any
|
||||||
|
:param action: the type of the action
|
||||||
|
:type action: NotificationAction, str
|
||||||
|
:raises TypeError: if ``actor`` is not a `Profile` object
|
||||||
|
:returns: the generated notification. It is already added to the database session, but
|
||||||
|
not committed
|
||||||
|
:rtype: Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(actor, Profile):
|
||||||
|
raise TypeError('actor must be a Profile instance')
|
||||||
|
|
||||||
|
if isinstance(action, str):
|
||||||
|
action = NotificationAction[action]
|
||||||
|
|
||||||
|
notification = Notification(profile=self, actor=actor, item=item, action=action)
|
||||||
|
|
||||||
|
return notification
|
||||||
|
|
||||||
|
|
||||||
class Event(db.Model):
|
class Event(db.Model):
|
||||||
"""Database model for events
|
"""Database model for events
|
||||||
@@ -400,6 +465,20 @@ class Event(db.Model):
|
|||||||
|
|
||||||
return url_for('event_details', event_uuid=self.event_uuid)
|
return url_for('event_details', event_uuid=self.event_uuid)
|
||||||
|
|
||||||
|
def invite(self, inviter, invited):
|
||||||
|
"""Invite ``invited`` to the event
|
||||||
|
|
||||||
|
The invitation will arrive from ``inviter``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
invite = Invitation(event=self, sender=inviter, invitee=invited)
|
||||||
|
db.session.add(invite)
|
||||||
|
|
||||||
|
notification = invited.notify(inviter, self, NotificationAction.invite)
|
||||||
|
db.session.add(notification)
|
||||||
|
|
||||||
|
return invite
|
||||||
|
|
||||||
|
|
||||||
class UserSetting(db.Model): # pylint: disable=too-few-public-methods
|
class UserSetting(db.Model): # pylint: disable=too-few-public-methods
|
||||||
"""Database model for user settings
|
"""Database model for user settings
|
||||||
@@ -543,6 +622,12 @@ class UserFollow(db.Model): # pylint: disable=too-few-public-methods
|
|||||||
#: The timestamp when the follow was accepted
|
#: The timestamp when the follow was accepted
|
||||||
accepted_at = db.Column(db.DateTime(), nullable=True)
|
accepted_at = db.Column(db.DateTime(), nullable=True)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Accept this follow request
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.accepted_at = datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
class Notification(db.Model):
|
class Notification(db.Model):
|
||||||
"""Database model for notifications
|
"""Database model for notifications
|
||||||
|
13
calsocial/templates/follow-requests.html
Normal file
13
calsocial/templates/follow-requests.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% trans %}Follow requests{% endtrans %}</h2>
|
||||||
|
<ul>
|
||||||
|
{% for req in requests %}
|
||||||
|
<li>
|
||||||
|
{{ req.follower }}
|
||||||
|
<a href="{{ url_for('accept_follow', follower_id=req.follower_id) }}">{% trans %}Accept{% endtrans %}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
@@ -120,7 +120,7 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
<td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}">
|
<td class="{% if day.month != calendar.timestamp.month %} other-month{% endif %}{% if day.date() == now.date() %} today{% endif %}">
|
||||||
<span class="day-num">{{ day.day }}</span>
|
<span class="day-num">{{ day.day }}</span>
|
||||||
{% for event in calendar.day_events(day, user=current_user) %}
|
{% for event in calendar.day_events(day, user=current_user if user_only else none) %}
|
||||||
<a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event">
|
<a href="{{ url_for('event_details', event_uuid=event.event_uuid) }}" class="event">
|
||||||
{{ 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') }}
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
|
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
{{ profile.name }}
|
{% if profile.locked %}
|
||||||
|
[locked]
|
||||||
|
{% 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 %}
|
||||||
|
22
calsocial/templates/profile-edit.html
Normal file
22
calsocial/templates/profile-edit.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends 'settings-base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ super() }}
|
||||||
|
<h2>{% trans %}Edit profile{% endtrans %}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ form.errors }}
|
||||||
|
|
||||||
|
{{ form.display_name.errors }}
|
||||||
|
{{ form.display_name.label }}
|
||||||
|
{{ form.display_name }}
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{ form.locked.errors }}
|
||||||
|
{{ form.locked.label }}
|
||||||
|
{{ form.locked}}
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<button type="submit">{% trans %}Save{% endtrans %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
10
calsocial/templates/settings-base.html
Normal file
10
calsocial/templates/settings-base.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ url_for('edit_profile') }}">{% trans %}Edit profile{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url_for('settings') }}">{% trans %}Settings{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
@@ -1,6 +1,8 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'settings-base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{{ super() }}
|
||||||
|
<h2>{% trans %}Settings{% endtrans %}</h2>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
72
tests/helpers.py
Normal file
72
tests/helpers.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
@@ -1,23 +1,23 @@
|
|||||||
import pytest
|
# 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/>.
|
||||||
|
|
||||||
import calsocial
|
"""General tests for Calendar.social
|
||||||
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):
|
||||||
@@ -26,96 +26,3 @@ def test_index_no_login(client):
|
|||||||
|
|
||||||
page = client.get('/')
|
page = client.get('/')
|
||||||
assert b'Welcome to Calendar.social' 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
|
|
||||||
|
117
tests/test_follow.py
Normal file
117
tests/test_follow.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Calendar.social
|
||||||
|
# Copyright (C) 2018 Gergely Polonkai
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Profile related tests for Calendar.social
|
||||||
|
"""
|
||||||
|
|
||||||
|
import calsocial
|
||||||
|
from calsocial.models import db, Notification, NotificationAction, Profile, User, UserFollow
|
||||||
|
|
||||||
|
from helpers import 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
|
84
tests/test_login.py
Normal file
84
tests/test_login.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 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_index_no_login(client):
|
||||||
|
"""Test the main page without logging in
|
||||||
|
"""
|
||||||
|
|
||||||
|
page = client.get('/')
|
||||||
|
assert b'Welcome to Calendar.social' in page.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_invalid_user(client):
|
||||||
|
"""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/first-steps'
|
116
tests/test_register.py
Normal file
116
tests/test_register.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
Reference in New Issue
Block a user