Skip to content

Commit db14476

Browse files
author
Michael Zhang
committed
Merge branch 'master' of github.com:easyctf/ctf-calendar
2 parents ccec004 + ca699b2 commit db14476

14 files changed

+200
-55
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
.env
55
.secret_key
66
npm-debug.log
7+
.coverage
8+
.cache

.travis.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
language: python
2+
sudo: required
3+
python:
4+
- "2.7"
5+
install:
6+
- sudo apt-get update
7+
- sudo apt-get -y install python python-dev python-pip
8+
- pip install -r requirements.txt
9+
- pip install coveralls
10+
before_script:
11+
- psql -c 'create database ctfcal;' -U postgres
12+
services:
13+
- postgresql
14+
script:
15+
- coverage run --source . -m pytest -v && coverage report
16+
after_success:
17+
coveralls
18+
notifications:
19+
email: false

config.py

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
load_dotenv(find_dotenv())
77

8+
EVENT_LIST_PAGE_SIZE = 25
9+
USER_LIST_PAGE_SIZE = 50
10+
811

912
class CalendarConfig:
1013
def __init__(self, app_root=None, testing=False):

conftest.py

Whitespace-only changes.

forms.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ def validate_username(self, field):
5050

5151
class EventForm(Form):
5252
title = StringField('Title', validators=[InputRequired(), Length(max=256)])
53-
start_time = IntegerField('Start Time', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
53+
start_time = IntegerField('Start Time (UNIX Time)', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
5454
message='Start time must be between 0 and 2147483647!')])
55-
duration = FloatField('Duration (hours)', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
55+
duration = FloatField('Duration (Hours)', validators=[InputRequired(), NumberRange(min=0, max=2147483647,
5656
message='Duration must be between 0 and 2147483647!')])
5757
description = StringField('Description', widget=TextArea(), validators=[InputRequired(), Length(max=1024)])
5858
link = StringField('Link', validators=[InputRequired(), Length(max=256)])

models.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timedelta
2+
from functools import partial
23

34
from flask_login import current_user, LoginManager
45
from flask_oauthlib.provider import OAuth2Provider
@@ -92,18 +93,30 @@ class Event(db.Model):
9293
approved = db.Column(db.Boolean, default=False)
9394
title = db.Column(db.Unicode(length=256))
9495
start_time = db.Column(db.Integer, index=True)
95-
duration = db.Column(db.Float)
96+
duration = db.Column(db.Float) # in hours
9697
description = db.Column(db.UnicodeText)
9798
link = db.Column(db.Unicode(length=256))
9899
removed = db.Column(db.Boolean, default=False)
99100

100101
# OAuth2 stuff
101-
client_id = db.Column(db.String(40), unique=True, default=util.generate_string(16))
102-
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False, default=util.generate_string(32))
102+
client_id = db.Column(db.String(40), unique=True, default=partial(util.generate_string, 16))
103+
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False, default=partial(util.generate_string, 32))
103104
is_confidential = db.Column(db.Boolean, default=True)
104105
_redirect_uris = db.Column(db.Text)
105106
_default_scopes = db.Column(db.Text)
106107

108+
@property
109+
def formatted_start_time(self):
110+
return util.isoformat(self.start_time)
111+
112+
@hybrid_property
113+
def end_time(self):
114+
return self.start_time + self.duration * 60
115+
116+
@property
117+
def formatted_end_time(self):
118+
return util.isoformat(self.end_time)
119+
107120
@property
108121
def client_type(self):
109122
if self.is_confidential:

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ passlib
1313
pathlib
1414
psycopg2
1515
pymysql
16+
pytest
1617
python-dotenv
1718
sqlalchemy
1819
wtforms

templates/events/list.html

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<td>{{ event.description }}</td>
4747
<td>
4848
<time class="timeago"
49-
datetime="{{ event.start_time_format }}">{{ event.start_time_format }}</time>
49+
datetime="{{ event.formatted_start_time }}">{{ event.formatted_start_time }}</time>
5050
</td>
5151
<td>{{ event.duration|duration('hour') }}</td>
5252
<td><a href="{{ event.link }}" target="_blank">Website</a></td>
@@ -80,6 +80,14 @@
8080
</table>
8181
</div>
8282

83+
<p>
84+
{% if page_number == 1 %}Prev{% else %}
85+
<a href="{{ url_for('events.events_%s' % tab, page_number=page_number - 1) }}">Prev</a>{% endif %}
86+
/ Page <b>{{ page_number }}</b> /
87+
{% if last_page %}Next{% else %}
88+
<a href="{{ url_for('events.events_%s' % tab, page_number=page_number + 1) }}">Next</a>{% endif %}
89+
</p>
90+
8391
<script type="text/javascript">
8492
$(document).ready(function () {
8593
$.timeago.settings.allowFuture = true;

templates/layout.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@
3636
<ul class="dropdown-menu">
3737
<li><a href="{{ url_for('events.events_all') }}">All Events</a></li>
3838
{% if current_user.admin == true %}
39-
<li><a href="/events/unapproved">Unapproved Events</a></li>
39+
<li><a href="{{ url_for('events.events_unapproved') }}">Unapproved Events</a></li>
4040
{% endif %}
4141
<li role="separator" class="divider"></li>
42-
<li><a href="/events/upcoming">Upcoming Events</a></li>
43-
<li><a href="/events/past">Past Events</a></li>
42+
<li><a href="{{ url_for('events.events_upcoming') }}">Upcoming Events</a></li>
43+
<li><a href="{{ url_for('events.events_past') }}">Past Events</a></li>
4444
</ul>
4545
</li>
4646
<li class="dropdown">
4747
<a href="javascript:void(0);" class="dropdown-toggle" data-toggle="dropdown" role="button"
4848
aria-haspopup="true" aria-expanded="false">Community <span class="caret"></span></a>
4949
<ul class="dropdown-menu">
50-
<li><a href="/users">Find Players</a></li>
50+
<li><a href="{{ url_for('users.users_list') }}">Find Players</a></li>
5151
<li><a href="http://slack.easyctf.com" target="_blank">Slack</a></li>
5252
</ul>
5353
</li>

templates/users/list.html

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends "layout.html" %}
22

33
{% block title %}
4-
Event List
4+
User List
55
{% endblock %}
66

77
{% block content %}
@@ -15,4 +15,12 @@
1515
<br/>
1616
</div>
1717
{% endfor %}
18+
19+
<p>
20+
{% if page_number == 1 %}Prev{% else %}
21+
<a href="{{ url_for('users.users_list', page_number=page_number - 1) }}">Prev</a>{% endif %}
22+
/ Page <b>{{ page_number }}</b> /
23+
{% if last_page %}Next{% else %}
24+
<a href="{{ url_for('users.users_list', page_number=page_number + 1) }}">Next</a>{% endif %}
25+
</p>
1826
{% endblock %}

tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# hei

tests/test_general.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class TestGeneral():
2+
def test_sanity(self):
3+
assert "sanity level" > 0

views/events.py

+113-40
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
2+
import time
23

34
from flask import abort, Blueprint, redirect, render_template, url_for, flash
45
from flask_login import current_user, login_required
56

7+
import config
68
from forms import EventForm
79
from models import db, Event
810
from util import admin_required, isoformat
@@ -24,67 +26,137 @@ def events_create():
2426

2527

2628
@blueprint.route('/list/json')
27-
def events_list_json():
28-
events = Event.query.filter_by().order_by(Event.start_time.desc()).all()
29-
event_list = []
30-
for event in events:
31-
start_time = event.start_time
32-
obj = {
33-
'id': event.id,
34-
'approved': event.approved,
35-
'name': event.title,
36-
'startTime': start_time * 1000,
37-
'startTimeFormat': isoformat(start_time),
38-
'endTime': (start_time + event.duration * 60 * 60) * 1000,
39-
'duration': event.duration
40-
}
41-
event_list.append(obj)
29+
@blueprint.route('/list/json/page/<int:page_number>')
30+
def events_list_json(page_number=1):
31+
if page_number <= 0:
32+
abort(404)
33+
34+
page_size = config.EVENT_LIST_PAGE_SIZE
35+
page_offset = (page_number - 1) * page_size
36+
events = Event.query.order_by(Event.start_time.desc()).offset(page_offset).limit(page_size).all()
37+
if page_number != 1 and not events:
38+
abort(404)
39+
40+
event_list = [{
41+
'id': event.id,
42+
'approved': event.approved,
43+
'name': event.title,
44+
'startTime': event.start_time * 1000,
45+
'startTimeFormat': isoformat(event.start_time),
46+
'endTime': (event.start_time + event.duration * 60 * 60) * 1000,
47+
'duration': event.duration
48+
} for event in events]
4249
return json.dumps(event_list)
4350

4451

4552
@blueprint.route('/')
4653
@blueprint.route('/all')
47-
def events_all():
48-
events = Event.query.filter_by(approved=True, removed=False).order_by(Event.start_time.desc()).all()
49-
for event in events:
50-
event.start_time_format = isoformat(event.start_time)
51-
return render_template('events/list.html', tab='all', events=events)
54+
@blueprint.route('/all/page/<int:page_number>')
55+
def events_all(page_number=1):
56+
if page_number <= 0:
57+
abort(404)
58+
59+
page_size = config.EVENT_LIST_PAGE_SIZE
60+
page_offset = (page_number - 1) * page_size
61+
# Offset + limit for pagination is inefficient; implement page_start based pages if perf issues.
62+
events = Event.query.filter_by(approved=True, removed=False).order_by(Event.start_time.desc()) \
63+
.offset(page_offset).limit(page_size + 1).all()
64+
if page_number != 1 and not events:
65+
abort(404)
66+
67+
last_page = len(events) <= page_size
68+
if not last_page:
69+
events.pop()
70+
71+
return render_template('events/list.html', tab='all', page_number=page_number, last_page=last_page, events=events)
5272

5373

5474
# todo
5575
@blueprint.route('/upcoming')
56-
def events_upcoming():
57-
events = Event.query.filter_by(approved=True, removed=False).order_by(Event.start_time.desc()).all()
58-
for event in events:
59-
event.start_time_format = isoformat(event.start_time)
60-
return render_template('events/list.html', tab='upcoming', events=events)
76+
@blueprint.route('/upcoming/page/<int:page_number>')
77+
def events_upcoming(page_number=1):
78+
if page_number <= 0:
79+
abort(404)
80+
81+
page_size = config.EVENT_LIST_PAGE_SIZE
82+
page_offset = (page_number - 1) * page_size
83+
upcoming_events = Event.query.filter_by(approved=True, removed=False).filter(Event.start_time > time.time()) \
84+
.order_by(Event.start_time.desc()).offset(page_offset).limit(page_size + 1).all()
85+
if page_number != 1 and not upcoming_events:
86+
abort(404)
87+
88+
last_page = len(upcoming_events) <= page_size
89+
if not last_page:
90+
upcoming_events.pop()
91+
92+
return render_template('events/list.html', tab='upcoming', page_number=page_number, last_page=last_page,
93+
events=upcoming_events)
6194

6295

6396
# todo
6497
@blueprint.route('/past')
65-
def events_past():
66-
events = Event.query.filter_by(approved=True, removed=False).order_by(Event.start_time.desc()).all()
67-
for event in events:
68-
event.start_time_format = isoformat(event.start_time)
69-
return render_template('events/list.html', tab='past', events=events)
98+
@blueprint.route('/past/page/<int:page_number>')
99+
def events_past(page_number=1):
100+
if page_number <= 0:
101+
abort(404)
102+
103+
page_size = config.EVENT_LIST_PAGE_SIZE
104+
page_offset = (page_number - 1) * page_size
105+
past_events = Event.query.filter_by(approved=True, removed=False).filter(Event.end_time <= time.time()) \
106+
.order_by(Event.start_time.desc()).offset(page_offset).limit(page_size + 1).all()
107+
if page_number != 1 and not past_events:
108+
abort(404)
109+
110+
last_page = len(past_events) <= page_size
111+
if not last_page:
112+
past_events.pop()
113+
114+
return render_template('events/list.html', tab='past', page_number=page_number, last_page=last_page,
115+
events=past_events)
70116

71117

72118
@blueprint.route('/unapproved')
119+
@blueprint.route('/unapproved/page/<int:page_number>')
73120
@admin_required
74-
def events_unapproved():
75-
unapproved_events = Event.query.filter_by(approved=False, removed=False).order_by(Event.start_time.desc()).all()
76-
for event in unapproved_events:
77-
event.start_time_format = isoformat(event.start_time)
78-
return render_template('events/list.html', tab='unapproved', events=unapproved_events, enabled_actions=['approve'])
121+
def events_unapproved(page_number=1):
122+
if page_number <= 0:
123+
abort(404)
124+
125+
page_size = config.EVENT_LIST_PAGE_SIZE
126+
page_offset = (page_number - 1) * page_size
127+
unapproved_events = Event.query.filter_by(approved=False, removed=False).order_by(Event.start_time.desc()) \
128+
.offset(page_offset).limit(page_size + 1).all()
129+
if page_number != 1 and not unapproved_events:
130+
abort(404)
131+
132+
last_page = len(unapproved_events) <= page_size
133+
if not last_page:
134+
unapproved_events.pop()
135+
136+
return render_template('events/list.html', tab='unapproved', page_number=page_number, last_page=last_page,
137+
events=unapproved_events, enabled_actions=['approve'])
79138

80139

81140
@blueprint.route('/owned')
141+
@blueprint.route('/owned/page/<int:page_number>')
82142
@login_required
83-
def events_owned():
84-
owned_events = current_user.events.filter_by(removed=False)
85-
for event in owned_events:
86-
event.start_time_format = isoformat(event.start_time)
87-
return render_template('events/list.html', tab='owned', events=owned_events, enabled_actions=['manage', 'remove'])
143+
def events_owned(page_number=1):
144+
if page_number <= 0:
145+
abort(404)
146+
147+
page_size = config.EVENT_LIST_PAGE_SIZE
148+
page_offset = (page_number - 1) * page_size
149+
owned_events = current_user.events.filter_by(removed=False).order_by(Event.start_time.desc()) \
150+
.offset(page_offset).limit(page_size + 1).all()
151+
if page_number != 1 and not owned_events:
152+
abort(404)
153+
154+
last_page = len(owned_events) <= page_size
155+
if not last_page:
156+
owned_events.pop()
157+
158+
return render_template('events/list.html', tab='owned', page_number=page_number, last_page=last_page,
159+
events=owned_events, enabled_actions=['manage', 'remove'])
88160

89161

90162
@blueprint.route('/<int:event_id>')
@@ -115,6 +187,7 @@ def events_manage(event_id):
115187
event_form = EventForm(obj=event)
116188
if event_form.validate_on_submit():
117189
event_form.populate_obj(event)
190+
db.session.commit()
118191
return redirect(url_for('.events_detail', event_id=event_id))
119192
return render_template('events/manage.html', event=event, event_form=event_form)
120193

0 commit comments

Comments
 (0)