Compare commits
1 Commits
model-upda
...
drone-ci
Author | SHA1 | Date | |
---|---|---|---|
86f86996f6 |
17
.drone.yml
Normal file
17
.drone.yml
Normal 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
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +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/
|
||||||
|
@@ -170,7 +170,7 @@ class CalendarSocialApp(Flask):
|
|||||||
|
|
||||||
calendar = GregorianCalendar(timestamp.timestamp())
|
calendar = GregorianCalendar(timestamp.timestamp())
|
||||||
|
|
||||||
return render_template('index.html', calendar=calendar, user_only=True)
|
return render_template('index.html', calendar=calendar)
|
||||||
|
|
||||||
@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(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)
|
||||||
@@ -255,7 +255,7 @@ class CalendarSocialApp(Flask):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
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()
|
||||||
@@ -267,7 +267,15 @@ class CalendarSocialApp(Flask):
|
|||||||
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()
|
||||||
|
|
||||||
@@ -292,12 +300,11 @@ 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
|
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()
|
||||||
@@ -305,7 +312,16 @@ class CalendarSocialApp(Flask):
|
|||||||
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()
|
||||||
|
|
||||||
@@ -383,83 +399,5 @@ 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,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
|
|
||||||
|
@@ -103,24 +103,6 @@ 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
|
||||||
"""
|
"""
|
||||||
@@ -264,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 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
|
||||||
@@ -286,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})>'
|
||||||
|
|
||||||
@@ -311,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):
|
||||||
@@ -325,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):
|
||||||
@@ -340,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
|
||||||
@@ -465,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
|
||||||
@@ -622,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
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
{% 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 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">
|
||||||
{{ 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,10 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
{% if profile.locked %}
|
{{ profile.name }}
|
||||||
[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 %}
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
{% 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 %}
|
|
@@ -1,10 +0,0 @@
|
|||||||
{% 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,8 +1,6 @@
|
|||||||
{% extends 'settings-base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ super() }}
|
|
||||||
<h2>{% trans %}Settings{% endtrans %}</h2>
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
@@ -1,72 +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
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 @@
|
|||||||
# 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):
|
||||||
@@ -26,3 +26,96 @@ 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
|
||||||
|
@@ -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
|
|
@@ -1,84 +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_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'
|
|
@@ -1,116 +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
|
|
||||||
|
|
||||||
|
|
||||||
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