commit
821c6784e9
11 changed files with 282 additions and 92 deletions
62
README.md
62
README.md
|
@ -1,7 +1,7 @@
|
|||
Event Man(ager)
|
||||
===============
|
||||
|
||||
Manage attendants at an event.
|
||||
Your friendly manager of attendees at an event.
|
||||
|
||||
Notice
|
||||
======
|
||||
|
@ -10,11 +10,50 @@ No, this project is not ready, yet.
|
|||
|
||||
I'll let you know when I'm finished experimenting with it and you can contribute.
|
||||
|
||||
See the DEVELOPMENT.md file for more information about how to contribute.
|
||||
|
||||
|
||||
Technological stack
|
||||
===================
|
||||
|
||||
- [AngularJS](https://angularjs.org/) for the webApp
|
||||
- [Bootstrap](http://getbootstrap.com/) (plus [jQuery](https://jquery.com/)) for the eye-candy
|
||||
- [Tornado web](http://www.tornadoweb.org/) as web server
|
||||
- [MongoDB](https://www.mongodb.org/) to store the data
|
||||
|
||||
The web part is incuded; you need to install Tornado, MongoDB and the pymongo module on your system (no configuration needed).
|
||||
|
||||
|
||||
Coding style and conventions
|
||||
============================
|
||||
|
||||
It's enough to be consistent within the document you're editing.
|
||||
|
||||
I suggest four spaces instead of tabs for all the code: Python (**mandatory**), JavaScript, HTML and CSS.
|
||||
|
||||
Python code documented following the [Sphinx](http://sphinx-doc.org/) syntax.
|
||||
|
||||
|
||||
Install and run
|
||||
===============
|
||||
|
||||
wget https://bootstrap.pypa.io/get-pip.py
|
||||
sudo python get-pip.py
|
||||
sudo pip install tornado
|
||||
sudo pip install pymongo
|
||||
cd
|
||||
git clone https://github.com/raspibo/eventman
|
||||
cd eventman
|
||||
./eventman_server.py --debug
|
||||
|
||||
|
||||
Open browser and navigate to: http://localhost:5242/
|
||||
|
||||
|
||||
License and copyright
|
||||
=====================
|
||||
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
RaspiBO <info@raspibo.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -27,22 +66,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Install and run
|
||||
===============
|
||||
|
||||
wget https://bootstrap.pypa.io/get-pip.py
|
||||
|
||||
sudo python get-pip.py
|
||||
|
||||
sudo pip install tornado
|
||||
|
||||
cd
|
||||
|
||||
git clone https://github.com/raspibo/eventman
|
||||
|
||||
cd eventman
|
||||
|
||||
./eventman_server.py
|
||||
|
||||
|
||||
Open browser and navigate to: http://localhost:5242/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- show details of a single Event (editing also take place here) -->
|
||||
<div class="container-fluid">
|
||||
<form ng-model="eventdetails" ng-submit="save()">
|
||||
<div class="input-group input-group-lg">
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
<div class="container-fluid">
|
||||
<!-- show a list of Events -->
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="title">Alphabetical</option>
|
||||
<option value="begin-datetime">Date</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="title">Alphabetical</option>
|
||||
<option value="begin-datetime">Date</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
|
||||
<ul class="events">
|
||||
<li ng-repeat="event in events | filter:query | orderBy:orderProp">
|
||||
<span><a href="/#/events/{{event._id}}">{{event.title}}</a></span>
|
||||
<p>{{event['begin-datetime']}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<ul class="events">
|
||||
<li ng-repeat="event in events | filter:query | orderBy:orderProp">
|
||||
<span><a href="/#/events/{{event._id}}">{{event.title}}</a></span>
|
||||
<p>{{event['begin-datetime']}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<!doctype html>
|
||||
<html ng-app="eventManApp">
|
||||
<head>
|
||||
<title>Event Man(ager)</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
@ -10,11 +11,25 @@
|
|||
<script src="/js/app.js"></script>
|
||||
<script src="/js/services.js"></script>
|
||||
<script src="/js/controllers.js"></script>
|
||||
<title>Event Man(ager)</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/css/bootstrap-theme.min.css" rel="stylesheet">
|
||||
<link href="/static/css/eventman.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<!--
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
RaspiBO <info@raspibo.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<body>
|
||||
<div class="main-header" ng-controller="navigation as n">
|
||||
|
@ -24,6 +39,8 @@
|
|||
<input type="button" id="persons-button" ng-click="n.go('/new-person')" class="btn btn-link" value="Add Persons" />
|
||||
</div>
|
||||
|
||||
<!-- all the magic takes place here: the content inside the next div
|
||||
changes accordingly to the location you're visiting -->
|
||||
<div ng-view></div>
|
||||
|
||||
<div class="main-footer">
|
||||
|
|
23
angular_app/js/app.js
vendored
23
angular_app/js/app.js
vendored
|
@ -1,3 +1,21 @@
|
|||
'use strict';
|
||||
/*
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
RaspiBO <info@raspibo.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/* Register our fantastic app. */
|
||||
var eventManApp = angular.module('eventManApp', [
|
||||
'ngRoute',
|
||||
'eventManServices',
|
||||
|
@ -5,6 +23,7 @@ var eventManApp = angular.module('eventManApp', [
|
|||
]);
|
||||
|
||||
|
||||
/* Directive that can be used to make an input field react to the press of Enter. */
|
||||
eventManApp.directive('ngEnter', function () {
|
||||
return function (scope, element, attrs) {
|
||||
element.bind("keydown keypress", function (event) {
|
||||
|
@ -19,6 +38,7 @@ eventManApp.directive('ngEnter', function () {
|
|||
});
|
||||
|
||||
|
||||
/* Configure the routes. */
|
||||
eventManApp.config(['$routeProvider',
|
||||
function($routeProvider) {
|
||||
$routeProvider.
|
||||
|
@ -49,5 +69,6 @@ eventManApp.config(['$routeProvider',
|
|||
otherwise({
|
||||
redirectTo: '/events'
|
||||
});
|
||||
}]);
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
16
angular_app/js/controllers.js
vendored
16
angular_app/js/controllers.js
vendored
|
@ -1,9 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
/* Controllers */
|
||||
/* Controllers; their method are available where specified with the ng-controller
|
||||
* directive or for a given route (see app.js). They use some services to
|
||||
* connect to the backend (see services.js). */
|
||||
var eventManControllers = angular.module('eventManControllers', []);
|
||||
|
||||
|
||||
/* A controller that can be used to navigate. */
|
||||
eventManControllers.controller('navigation', ['$location',
|
||||
function ($location) {
|
||||
this.go = function(url) {
|
||||
|
@ -26,11 +29,12 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', 'Event', '$routePa
|
|||
if ($routeParams.id) {
|
||||
$scope.event = Event.get($routeParams);
|
||||
}
|
||||
// store a new Event or update an existing one
|
||||
$scope.save = function() {
|
||||
if ($scope.event.id === undefined) {
|
||||
Event.save($scope.event);
|
||||
$scope.event = Event.save($scope.event);
|
||||
} else {
|
||||
Event.update($scope.event);
|
||||
$scope.event = Event.update($scope.event);
|
||||
}
|
||||
};
|
||||
}]
|
||||
|
@ -50,11 +54,13 @@ eventManControllers.controller('PersonDetailsCtrl', ['$scope', 'Person', '$route
|
|||
if ($routeParams.id) {
|
||||
$scope.person = Person.get($routeParams);
|
||||
}
|
||||
// store a new Person or update an existing one
|
||||
$scope.save = function() {
|
||||
$scope.save = function() {
|
||||
if ($scope.person.id === undefined) {
|
||||
Person.save($scope.person);
|
||||
$scope.person = Person.save($scope.person);
|
||||
} else {
|
||||
Person.update($scope.person);
|
||||
$scope.person = Person.update($scope.person);
|
||||
}
|
||||
};
|
||||
}]
|
||||
|
|
4
angular_app/js/services.js
vendored
4
angular_app/js/services.js
vendored
|
@ -1,5 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
/* Services that are used to interact with the backend. */
|
||||
var eventManServices = angular.module('eventManServices', ['ngResource']);
|
||||
|
||||
|
||||
eventManServices.factory('Event', ['$resource',
|
||||
function($resource) {
|
||||
return $resource('events/:id', {id: '@_id'}, {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- show details of a single Person (editing also take place here) -->
|
||||
<div class="container-fluid">
|
||||
<form ng-model="persondetails" ng-submit="save()">
|
||||
<div class="input-group input-group-lg">
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
<div class="container-fluid">
|
||||
<!-- show a list of Persons -->
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="id">ID</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="id">ID</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
|
||||
<ul class="persons">
|
||||
<li ng-repeat="person in persons | filter:query | orderBy:orderProp">
|
||||
<a href="/#/persons/{{person._id}}"><span>{{person.name}}</span> <span>{{person.surname}}</span></a>
|
||||
<p>{{person.email}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<ul class="persons">
|
||||
<li ng-repeat="person in persons | filter:query | orderBy:orderProp">
|
||||
<a href="/#/persons/{{person._id}}"><span>{{person.name}}</span> <span>{{person.surname}}</span></a>
|
||||
<p>{{person.email}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
90
backend.py
90
backend.py
|
@ -1,6 +1,19 @@
|
|||
"""Event Man(ager) backend
|
||||
"""Event Man(ager) database backend
|
||||
|
||||
Classes and functions used to manage events and attendants.
|
||||
Classes and functions used to manage events and attendees database.
|
||||
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
RaspiBO <info@raspibo.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import pymongo
|
||||
|
@ -8,15 +21,29 @@ from bson.objectid import ObjectId
|
|||
|
||||
|
||||
class EventManDB(object):
|
||||
"""MongoDB connector."""
|
||||
db = None
|
||||
connection = None
|
||||
|
||||
def __init__(self, url=None, dbName='eventman'):
|
||||
"""Initialize the instance, connecting to the database.
|
||||
|
||||
:param url: URL of the database
|
||||
:type url: str (or None to connect to localhost)
|
||||
"""
|
||||
self._url = url
|
||||
self._dbName = dbName
|
||||
self.connect(url)
|
||||
|
||||
def connect(self, url=None, dbName=None):
|
||||
"""Connect to the database.
|
||||
|
||||
:param url: URL of the database
|
||||
:type url: str (or None to connect to localhost)
|
||||
|
||||
:return: the database we're connected to
|
||||
:rtype: :class:`~pymongo.database.Database`
|
||||
"""
|
||||
if self.db is not None:
|
||||
return self.db
|
||||
if url:
|
||||
|
@ -28,12 +55,32 @@ class EventManDB(object):
|
|||
return self.db
|
||||
|
||||
def get(self, collection, _id):
|
||||
"""Get a single document with the specified `_id`.
|
||||
|
||||
:param collection: search the document in this collection
|
||||
:type collection: str
|
||||
:param _id: unique ID of the document
|
||||
:type _id: str or :class:`~bson.objectid.ObjectId`
|
||||
|
||||
:return: the document with the given `_id`
|
||||
:rtype: dict
|
||||
"""
|
||||
if not isinstance(_id, ObjectId):
|
||||
_id = ObjectId(_id)
|
||||
results = self.query(collection, {'_id': _id})
|
||||
return results and results[0] or {}
|
||||
|
||||
def query(self, collection, query=None):
|
||||
"""Get multiple documents matching a query.
|
||||
|
||||
:param collection: search for documents in this collection
|
||||
:type collection: str
|
||||
:param query: search for documents with those attributes
|
||||
:type query: dict or None
|
||||
|
||||
:return: list of matching documents
|
||||
:rtype: list
|
||||
"""
|
||||
db = self.connect()
|
||||
query = query or {}
|
||||
if'_id' in query and not isinstance(query['_id'], ObjectId):
|
||||
|
@ -44,11 +91,33 @@ class EventManDB(object):
|
|||
return results
|
||||
|
||||
def add(self, collection, data):
|
||||
"""Insert a new document.
|
||||
|
||||
:param collection: insert the document in this collection
|
||||
:type collection: str
|
||||
:param data: the document to store
|
||||
:type data: dict
|
||||
|
||||
:return: the document, as created in the database
|
||||
:rtype: dict
|
||||
"""
|
||||
db = self.connect()
|
||||
_id = db[collection].insert(data)
|
||||
return self.get(collection, _id)
|
||||
|
||||
def update(self, collection, _id, data):
|
||||
"""Update an existing document.
|
||||
|
||||
:param collection: update a document in this collection
|
||||
:type collection: str
|
||||
:param _id: unique ID of the document to be updatd
|
||||
:type _id: str or :class:`~bson.objectid.ObjectId`
|
||||
:param data: the updated information to store
|
||||
:type data: dict
|
||||
|
||||
:return: the document, after the update
|
||||
:rtype: dict
|
||||
"""
|
||||
db = self.connect()
|
||||
data = data or {}
|
||||
if '_id' in data:
|
||||
|
@ -56,3 +125,20 @@ class EventManDB(object):
|
|||
db[collection].update({'_id': ObjectId(_id)}, {'$set': data})
|
||||
return self.get(collection, _id)
|
||||
|
||||
def delete(self, collection, _id_or_query=None, force=False):
|
||||
"""Remove one or more documents from a collection.
|
||||
|
||||
:param collection: search the documents in this collection
|
||||
:type collection: str
|
||||
:param _id_or_query: unique ID of the document or query to match multiple documents
|
||||
:type _id_or_query: str or :class:`~bson.objectid.ObjectId` or dict
|
||||
:param force: force the deletion of all documents, when `_id_or_query` is empty
|
||||
:type force: bool
|
||||
"""
|
||||
if not _id_or_query and not force:
|
||||
return
|
||||
db = self.connect()
|
||||
if not isinstance(_id_or_query, (ObjectId, dict)):
|
||||
_id_or_query = ObjectId(_id_or_query)
|
||||
db[collection].remove(_id_or_query)
|
||||
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
"""Event Man(ager)
|
||||
|
||||
Your friendly manager of attendants at a conference.
|
||||
Your friendly manager of attendees at an event.
|
||||
|
||||
Copyright 2015 Davide Alberani <da@erlug.linux.it>
|
||||
RaspiBO <info@raspibo.org>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
@ -18,58 +31,84 @@ from tornado import gen, escape
|
|||
import backend
|
||||
|
||||
|
||||
class BaseHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, **kwargs):
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
|
||||
@gen.coroutine
|
||||
def get(self):
|
||||
with open(self.angular_app_path + "/index.html", 'r') as fd:
|
||||
self.write(fd.read())
|
||||
|
||||
|
||||
class ImprovedEncoder(json.JSONEncoder):
|
||||
"""Enhance the default JSON encoder to serialize datetime objects."""
|
||||
def default(self, o):
|
||||
if isinstance(o, datetime.datetime):
|
||||
if isinstance(o, (datetime.datetime, datetime.date,
|
||||
datetime.time, datetime.timedelta)):
|
||||
return str(o)
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
json._default_encoder = ImprovedEncoder()
|
||||
|
||||
|
||||
class BaseHandler(tornado.web.RequestHandler):
|
||||
"""Base class for request handlers."""
|
||||
def initialize(self, **kwargs):
|
||||
"""Add every passed (key, value) as attributes of the instance."""
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
"""Handler for the / path."""
|
||||
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
|
||||
|
||||
@gen.coroutine
|
||||
def get(self):
|
||||
# serve the ./angular_app/index.html file
|
||||
with open(self.angular_app_path + "/index.html", 'r') as fd:
|
||||
self.write(fd.read())
|
||||
|
||||
|
||||
class CollectionHandler(BaseHandler):
|
||||
"""Base class for handlers that need to interact with the database backend.
|
||||
|
||||
Introduce basic CRUD operations."""
|
||||
# set of documents we're managing (a collection in MongoDB or a table in a SQL database)
|
||||
collection = None
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, id_=None):
|
||||
if id_ is not None:
|
||||
# read a single document
|
||||
self.write(self.db.get(self.collection, id_))
|
||||
else:
|
||||
# return an object containing the list of all objects in the collection;
|
||||
# e.g.: {'events': [{'_id': 'obj1-id, ...}, {'_id': 'obj2-id, ...}, ...]}
|
||||
# Please, never return JSON lists that are not encapsulated in an object,
|
||||
# to avoid XSS vulnerabilities.
|
||||
self.write({self.collection: self.db.query(self.collection)})
|
||||
|
||||
@gen.coroutine
|
||||
def post(self, id_=None, **kwargs):
|
||||
data = escape.json_decode(self.request.body or {})
|
||||
if id_ is None:
|
||||
# insert a new document
|
||||
newData = self.db.add(self.collection, data)
|
||||
else:
|
||||
# update an existing document
|
||||
newData = self.db.update(self.collection, id_, data)
|
||||
self.write(newData)
|
||||
|
||||
# PUT is handled by the POST method
|
||||
put = post
|
||||
|
||||
|
||||
class PersonsHandler(CollectionHandler):
|
||||
"""Handle requests for Persons."""
|
||||
collection = 'persons'
|
||||
|
||||
|
||||
class EventsHandler(CollectionHandler):
|
||||
"""Handle requests for Events."""
|
||||
collection = 'events'
|
||||
|
||||
|
||||
def main():
|
||||
def run():
|
||||
"""Run the Tornado web application."""
|
||||
# command line arguments; can also be written in a configuration file,
|
||||
# specified with the --config argument.
|
||||
define("port", default=5242, help="run on the given port", type=int)
|
||||
define("data", default=os.path.join(os.path.dirname(__file__), "data"),
|
||||
help="specify the directory used to store the data")
|
||||
|
@ -82,6 +121,7 @@ def main():
|
|||
callback=lambda path: tornado.options.parse_config_file(path, final=False))
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
# database backend connector
|
||||
db_connector = backend.EventManDB(url=options.mongodbURL, dbName=options.dbName)
|
||||
init_params = dict(db=db_connector)
|
||||
|
||||
|
@ -100,5 +140,5 @@ def main():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
run()
|
||||
|
||||
|
|
Loading…
Reference in a new issue