Skip to content

Commit e935460

Browse files
Merge pull request #42 from RainbowRevenge/master
ready for v2.0.1
2 parents f3c1915 + e7001ae commit e935460

28 files changed

+308
-285
lines changed

CHANGELOG.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# Changelog
22

3-
## 2.0.1 (tbd)
3+
## 2.0.1 (25-02-2022)
44

55
### Added
66

7+
- `flask create-upload-file` automatically create a upload file containing filepaths from a directory
8+
- Polygon Task: Draw and modify polygons on an image.
9+
- New examples for annotation protocols
10+
- Freetext task allowing input of an arbitrary string
11+
712
### Removed
813

914
### Changed
@@ -12,6 +17,11 @@
1217
- node is only required for development from now on
1318
- dist folder contains minified js and css
1419
- when developing client side node is still needed for webpack
20+
- transfer source code out of static folder
21+
- client source code is now in separate /client folder
22+
- moved package.json and webpack scripts to /client
23+
- npm scripts have to be started from /client
24+
- transpiled, minified js and css files and assets remain in /app/static/dist
1525

1626
## 2.0.0 (13-01-2022)
1727

Dockerfile

-30
This file was deleted.

README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,18 @@ HUMAN is an annotation server that stands for...
99

1010
# Try our [DEMO](http://human.lsv.uni-saarland.de)!
1111

12-
13-
1412
## Installation and Setup
1513

1614
The only requirement is a working version of python 3.9.x. Using anaconda or [https://docs.conda.io/en/latest/miniconda.html](miniconda) for a python environment is highly recommended.
1715

1816
The setup consists of 5 parts:
17+
1918
1. [Python environment](#python-environment)
2019
2. [Database setup](#database-setup)
2120
3. [Annotation protocol](#annotation-protocol)
2221
4. [Deploy server](#deploy-server)
2322
5. [Add data](#add-data)
2423

25-
2624
### Python environment
2725

2826
First install the python environment.
@@ -71,20 +69,23 @@ flask run reset-annotations
7169

7270
This will reset the annotations table in the database and is necessary to properly save annotations after a change in the protocol.
7371

74-
7572
### Deploy server
7673

7774
To run a server you have 2 possibilities:
7875

7976
#### 1. Run locally on your machine
80-
Locally you can start a server with
77+
78+
Locally you can start a server with
79+
8180
```sh
82-
flask start
81+
flask run
8382
```
83+
8484
and visit http://127.0.0.1:5000 in your favorite browser and login with your admin account.
8585
However it is very ill advised to use this with an open port in a production environment.
8686

8787
#### 2. Deploy in a production environment
88+
8889
When running HUMAN in a production environment on a server we recommend using gunicorn (included in the environment). First, you should set a secure SECRET_KEY in config.py. The script `start_server.sh` should take care of starting the server.
8990

9091
For docker refer to the [https://github.com/uds-lsv/human/wiki/Docker/](wiki).

app/automaton.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ def save(self, data=None, **kwargs):
6060

6161

6262
def write_to_db(self, **kwargs):
63-
print('write to db', self.annotations)
64-
self.to_start()
65-
return
63+
# print('write to db', self.annotations)
64+
# self.to_start()
65+
# return
6666
data = self.annotations
67-
# copy of write to db
67+
# copy of write to db in routes.py
6868
if not data:
6969
return "No Annotations"
7070
else:
@@ -94,7 +94,7 @@ def write_to_db(self, **kwargs):
9494
# Unconditionally set. If the user completed an annotation, it was current_annotation.
9595
db.execute("UPDATE user SET current_annotation = 0 WHERE id = ?", (current_user.get_id(),))
9696

97-
# db.commit() # TODO comment in again
97+
db.commit() # TODO comment in again
9898
# transition to start
9999
self.to_start()
100100

app/db.py

+98-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def get_db():
2222
current_app.config['DATABASE'],
2323
detect_types=sqlite3.PARSE_DECLTYPES
2424
)
25+
g.db.execute("PRAGMA foreign_keys=ON") # prevents removing users or data that has annotations
2526
g.db.row_factory = sqlite3.Row
2627

2728
return g.db
@@ -41,6 +42,14 @@ def init_db():
4142
"""Clear existing data and create new tables."""
4243
db = get_db()
4344

45+
# drop tables
46+
# db.execute("PRAGMA foreign_keys=OFF") # allows removing users or data that has annotations
47+
db.execute('DROP TABLE IF EXISTS annotations;')
48+
db.execute('DROP TABLE IF EXISTS user;')
49+
db.execute('DROP TABLE IF EXISTS data;')
50+
db.execute('DROP TABLE IF EXISTS options;')
51+
52+
# load defaults for tables
4453
with current_app.open_resource('schema.sql') as f:
4554
db.executescript(f.read().decode('utf8'))
4655

@@ -53,7 +62,12 @@ def columns_from_automaton():
5362
db.execute(f'ALTER TABLE annotations ADD COLUMN "{column}" text;')
5463
db.commit()
5564

56-
def save_db(table):
65+
def save_db(table: str ):
66+
"""
67+
Save the specified table as csv.
68+
69+
If argument table="all" save all tables
70+
"""
5771
db = get_db()
5872
if table=="all":
5973
tables = db.execute("SELECT name FROM sqlite_master WHERE type='table';").fetchall()
@@ -92,6 +106,36 @@ def init_db_command():
92106

93107
click.echo('Initialized the database.')
94108

109+
110+
@click.command('reset-annotations')
111+
@with_appcontext
112+
def reset_annotations_command():
113+
"""Update annotations table columns. Previous table has to be dropped for this change"""
114+
if not os.path.isfile(current_app.config['DATABASE']):
115+
init_db()
116+
else:
117+
while True:
118+
create = input('This will drop the annotations table! It will be backed up in the ./output folder. y/n: ')
119+
if create == 'no' or create == 'n':
120+
return
121+
elif create == 'yes' or create == 'y':
122+
break
123+
save_db('annotations')
124+
reset_table('annotations')
125+
columns_from_automaton()
126+
127+
click.echo('Initialized the database.')
128+
129+
def reset_table(table):
130+
"""Drop specified table and load default table from schema.sql"""
131+
db = get_db()
132+
# drop table
133+
db.execute(f'DROP TABLE IF EXISTS {table}')
134+
# load defaults for dropped table(s)
135+
with current_app.open_resource('schema.sql') as f:
136+
db.executescript(f.read().decode('utf8'))
137+
db.commit()
138+
95139
@click.command('db-from-csv')
96140
@click.argument('csv')
97141
@click.argument('table')
@@ -107,7 +151,7 @@ def db_from_csv_command(csv, table):
107151
@with_appcontext
108152
def add_admin():
109153
"""
110-
Function that shows prompt for user to add an Admin user to the database.
154+
Starts prompt for user to add an Admin user to the database.
111155
"""
112156
db = get_db()
113157
db_cursor = db.cursor()
@@ -138,6 +182,7 @@ def add_admin():
138182
@click.option('-dataid', '--data_id')
139183
@with_appcontext
140184
def remove_all_annotation_for_data(data_id):
185+
"""Remove all annotations for a given data id"""
141186
db = get_db()
142187
db_cursor = db.cursor()
143188

@@ -162,7 +207,6 @@ def remove_all_annotation_for_data(data_id):
162207
# db_cursor.execute("INSERT INTO user (username, email, given_name, surname, password, user_type, is_approved, annotated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
163208
# (username, email, given_name, surname, password_hash, user_type, is_approved, annotated))
164209

165-
166210
db.commit()
167211
db.close()
168212
click.echo("Successfully removed.")
@@ -171,6 +215,7 @@ def remove_all_annotation_for_data(data_id):
171215
@click.option('-id', '--annotation_id')
172216
@with_appcontext
173217
def remove_single_annotation(annotation_id):
218+
"""Remove a single annotation given the annotation id"""
174219
db = get_db()
175220
db_cursor = db.cursor()
176221
if annotation_id:
@@ -183,9 +228,11 @@ def remove_single_annotation(annotation_id):
183228
click.echo("Successfully removed.")
184229

185230
def remove_annotation(db_cursor, annotation_id):
186-
231+
"""Utility function to remove a annotation given a annotation id"""
232+
# find annotation and data rows for annotation id
187233
annotation = db_cursor.execute("SELECT * from annotations WHERE id=?", (annotation_id,)).fetchone()
188234
data = db_cursor.execute("SELECT * from data WHERE id=?", (annotation["data_id"],)).fetchone()
235+
# decrement annotation count in data table
189236
annotation_count = data['annotation_count']
190237
if annotation_count <= 0:
191238
annotation_count = 0
@@ -194,12 +241,57 @@ def remove_annotation(db_cursor, annotation_id):
194241
db_cursor.execute("UPDATE data SET annotation_count=? WHERE id=?",
195242
(annotation_count, annotation["data_id"]))
196243

244+
# remove annotation from user table
197245
user = db_cursor.execute("SELECT * from user WHERE id=?", (annotation["user_id"],)).fetchone()
198246
annotated = " ".join([anno for anno in user['annotated'].split() if int(anno) != annotation["data_id"]])
199247
db_cursor.execute("UPDATE user SET annotated=? WHERE id=?", (annotated, annotation["user_id"]))
200248
db_cursor.execute("DELETE FROM annotations WHERE id=?",
201249
(annotation_id,))
202250

251+
@click.command('create-upload-file')
252+
@click.argument('path')
253+
@click.argument('output', default="upload.tsv", )
254+
@click.option('-s', '--suffix', help="Include only files with suffix")
255+
@click.option('--fullpath', is_flag=True, help="Add full path from root to file: /root/to/file")
256+
@click.option('--includedir', is_flag=True, help="Add enclosing folder to file: parent/file")
257+
@with_appcontext
258+
def create_upload_file_command(path, output, suffix, fullpath, includedir):
259+
"""
260+
Create an upload file containing all files in a path. Context and Meta fields are empty
261+
262+
Arguments:
263+
PATH: containing files to list in upload file;
264+
OUTPUT: Path to output file.
265+
"""
266+
if not os.path.isdir(path):
267+
click.echo(f'{path} is not a valid path.')
268+
return
269+
if fullpath and includedir:
270+
click.echo(f'only one of fullpath or includedir allowed')
271+
return
272+
# header
273+
str_content = 'content\tcontext\tmeta\n'
274+
str_join = '\t\t\n'
275+
# default
276+
output = (output if output else "upload.tsv")
277+
suffix = (suffix if suffix else "")
278+
parent_path = (os.path.basename(os.path.realpath(path)) if includedir else "")
279+
parent_path = (os.path.realpath(path) if fullpath else parent_path)
280+
281+
# get filenames
282+
filenames = [
283+
os.path.join(parent_path, f) for f in os.listdir(path)
284+
if os.path.isfile(os.path.join(path, f))
285+
and (f.lower().endswith(suffix))
286+
]
287+
str_content += str_join.join(filenames)
288+
289+
with open(output, 'w') as f:
290+
f.write(str_content)
291+
click.echo(f"Wrote into {output}")
292+
293+
294+
203295
def init_app(app):
204296
"""Register database functions with the Flask app. This is called by
205297
the application factory.
@@ -211,3 +303,5 @@ def init_app(app):
211303
app.cli.add_command(add_admin)
212304
app.cli.add_command(remove_all_annotation_for_data)
213305
app.cli.add_command(remove_single_annotation)
306+
app.cli.add_command(reset_annotations_command)
307+
app.cli.add_command(create_upload_file_command)

app/routes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ def home():
766766
Route for home page
767767
"""
768768
current_app.logger.debug("from / (home_page)")
769-
return render_template('annotation_page.html', user=current_user.username, admin=current_user.admin)
769+
return render_template('annotation_page.html', user=current_user.username, admin=current_user.admin, reverse=current_app.config['SIDEBAR'])
770770

771771

772772
########## MISC Functions

app/schema.sql

+19-24
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,31 @@
1-
DROP TABLE IF EXISTS user;
2-
DROP TABLE IF EXISTS data;
3-
DROP TABLE IF EXISTS annotations;
4-
DROP TABLE IF EXISTS options;
5-
61
CREATE TABLE IF NOT EXISTS user
72
(
8-
id integer primary key autoincrement not null,
9-
username text not null,
10-
email text not null,
11-
given_name text not null,
12-
surname text not null,
13-
password text not null,
14-
user_type text not null,
15-
is_approved text not null,
16-
annotated text not null,
17-
current_annotation integer not null default 0,
3+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
4+
username TEXT NOT NULL,
5+
email TEXT NOT NULL,
6+
given_name TEXT NOT NULL,
7+
surname TEXT NOT NULL,
8+
password TEXT NOT NULL,
9+
user_type TEXT NOT NULL,
10+
is_approved TEXT NOT NULL,
11+
annotated TEXT NOT NULL,
12+
current_annotation INTEGER NOT NULL DEFAULT 0,
1813
automaton blob);
1914

2015
CREATE TABLE IF NOT EXISTS data
2116
(
22-
id integer primary key autoincrement not null,
23-
content text not null,
24-
context text,
25-
meta text,
26-
annotation_count integer not null default 0);
17+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
18+
content TEXT NOT NULL,
19+
context TEXT,
20+
meta TEXT,
21+
annotation_count INTEGER NOT NULL DEFAULT 0);
2722

2823
CREATE TABLE IF NOT EXISTS annotations
2924
(
30-
id integer primary key autoincrement not null,
31-
data_id integer not null,
32-
user_id integer not null,
33-
timestamp timestamp default (strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) not null,
25+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
26+
data_id INTEGER NOT NULL REFERENCES data,
27+
user_id INTEGER NOT NULL REFERENCES user,
28+
timestamp timestamp DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) NOT NULL,
3429
timings
3530
);
3631

0 commit comments

Comments
 (0)