Skip to content

Commit d216adf

Browse files
authoredJan 17, 2022
Merge branch 'adjudication-master' into master
2 parents b56d43b + c567de1 commit d216adf

26 files changed

+3206
-189
lines changed
 

‎.gitignore

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Byte-compiled / optimized / DLL files
1+
# Byte-compiled / optimized / DLL files
22
__pycache__/
33
*.py[cod]
44
*$py.class
@@ -41,3 +41,8 @@ config.py
4141

4242
# exports
4343
*.xlsx
44+
45+
# logs
46+
**/*.log
47+
48+
setup.py

‎models.py

+188-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from sqlalchemy import Boolean, Column, Date, DateTime, Integer, String, Unicode, ForeignKey, UniqueConstraint, Text, UnicodeText
1+
from sqlalchemy import Column, Date, DateTime, Integer, String, Unicode, ForeignKey, UniqueConstraint, Text, UnicodeText
22
from sqlalchemy.orm import relationship, backref
33
from flask_login import UserMixin
4+
from sqlalchemy.sql.expression import desc
45
from database import Base
56
import datetime as dt
6-
import pytz
77
from pytz import timezone
88

99
central = timezone('US/Central')
@@ -29,6 +29,186 @@ def __init__(self, article_id, variable, value, coder_id, text = None):
2929
def __repr__(self):
3030
return '<CoderArticleAnnotation %r>' % (self.id)
3131

32+
33+
class CanonicalEvent(Base):
34+
__tablename__ = 'canonical_event'
35+
id = Column(Integer, primary_key=True)
36+
coder_id = Column(Integer, ForeignKey('user.id'), nullable = False)
37+
key = Column(Text, nullable = False)
38+
description = Column(UnicodeText, nullable = False)
39+
notes = Column(UnicodeText)
40+
last_updated = Column(DateTime)
41+
42+
UniqueConstraint('key', name = 'unique1')
43+
44+
def __init__(self, coder_id, key, description, notes = None):
45+
self.coder_id = coder_id
46+
self.key = key
47+
self.description = description,
48+
self.notes = notes
49+
self.last_updated = dt.datetime.now(tz = central).replace(tzinfo = None)
50+
51+
def __repr__(self):
52+
return '<CanonicalEvent %r>' % (self.id)
53+
54+
55+
class CanonicalEventLink(Base):
56+
__tablename__ = 'canonical_event_link'
57+
id = Column(Integer, primary_key=True)
58+
coder_id = Column(Integer, ForeignKey('user.id'), nullable = False)
59+
canonical_id = Column(Integer, ForeignKey('canonical_event.id'), nullable = False)
60+
cec_id = Column(Integer, ForeignKey('coder_event_creator.id'), nullable = False)
61+
timestamp = Column(DateTime)
62+
63+
UniqueConstraint('canonical_id', 'cec_id', name = 'unique1')
64+
65+
def __init__(self, coder_id, canonical_id, cec_id):
66+
self.coder_id = coder_id
67+
self.canonical_id = canonical_id
68+
self.cec_id = cec_id
69+
self.timestamp = dt.datetime.now(tz = central).replace(tzinfo = None)
70+
71+
def __repr__(self):
72+
return '<CanonicalEventLink %r>' % (self.id)
73+
74+
75+
class CanonicalEventRelationship(Base):
76+
__tablename__ = 'canonical_event_relationship'
77+
id = Column(Integer, primary_key=True)
78+
coder_id = Column(Integer, ForeignKey('user.id'))
79+
canonical_id1 = Column(Integer, ForeignKey('canonical_event.id'))
80+
canonical_id2 = Column(Integer, ForeignKey('canonical_event.id'))
81+
relationship_type = Column(Text)
82+
timestamp = Column(DateTime)
83+
84+
UniqueConstraint('canonical_id1', 'canonical_id2', 'relationship_type', name = 'unique1')
85+
86+
def __init__(self, coder_id, canonical_id1, canonical_id2, relationship_type):
87+
self.coder_id = coder_id
88+
self.canonical_id1 = canonical_id1
89+
self.canonical_id2 = canonical_id2
90+
self.relationship_type = relationship_type
91+
self.timestamp = dt.datetime.now(tz = central).replace(tzinfo = None)
92+
93+
def __repr__(self):
94+
return '<CanonicalEventRelationship %r -> %r (%r)>' % \
95+
(self.canonical_id1, self.canonical_id2, self.relationship_type)
96+
97+
98+
class EventFlag(Base):
99+
__tablename__ = 'event_flag'
100+
id = Column(Integer, primary_key=True)
101+
coder_id = Column(Integer, ForeignKey('user.id'))
102+
event_id = Column(Integer, ForeignKey('event.id'))
103+
flag = Column(Text)
104+
timestamp = Column(DateTime)
105+
106+
UniqueConstraint('event_id', name = 'unique1')
107+
108+
def __init__(self, coder_id, event_id, flag):
109+
self.coder_id = coder_id
110+
self.event_id = event_id
111+
self.flag = flag
112+
self.timestamp = dt.datetime.now(tz = central).replace(tzinfo = None)
113+
114+
def __repr__(self):
115+
return '<EventFlag %r (%r)>' % (self.event_id, self.flag)
116+
117+
118+
class EventMetadata(Base):
119+
__tablename__ = 'event_metadata'
120+
id = Column(Integer, primary_key=True)
121+
coder_id = Column(Integer, ForeignKey('user.id'))
122+
event_id = Column(Integer, ForeignKey('event.id'))
123+
article_id = Column(Integer, ForeignKey('article_metadata.id'))
124+
article_desc = Column(UnicodeText, nullable = True)
125+
desc = Column(UnicodeText, nullable = True)
126+
location = Column(Text, nullable = True)
127+
start_date = Column(Date, nullable = True)
128+
publication = Column(Text)
129+
pub_date = Column(Date)
130+
title = Column(Text)
131+
form = Column(Text)
132+
133+
UniqueConstraint('event_id', name = 'unique1')
134+
135+
def __init__(self, coder_id, event_id, article_id, article_desc, desc,
136+
location, start_date, publication, pub_date, title, form):
137+
self.coder_id = coder_id
138+
self.event_id = event_id
139+
self.article_id = article_id
140+
self.article_desc = article_desc
141+
self.desc = desc
142+
self.location = location
143+
self.start_date = start_date
144+
self.publication = publication
145+
self.pub_date = pub_date
146+
self.title = title
147+
self.form = form
148+
149+
def __repr__(self):
150+
return '<EventMetadata %r>' % (self.event_id)
151+
152+
153+
class RecentCanonicalEvent(Base):
154+
__tablename__ = 'recent_canonical_event'
155+
id = Column(Integer, primary_key=True)
156+
coder_id = Column(Integer, ForeignKey('user.id'))
157+
canonical_id = Column(Integer, ForeignKey('canonical_event.id'))
158+
last_accessed = Column(DateTime)
159+
160+
UniqueConstraint('coder_id', 'canonical_id', name = 'unique1')
161+
162+
def __init__(self, coder_id, canonical_id):
163+
self.coder_id = coder_id
164+
self.canonical_id = canonical_id
165+
self.last_accessed = dt.datetime.now(tz = central).replace(tzinfo = None)
166+
167+
def __repr__(self):
168+
return '<RecentCanonicalEvent %r (%r)>' % (self.canonical_id, self.last_accessed)
169+
170+
171+
class RecentEvent(Base):
172+
__tablename__ = 'recent_event'
173+
id = Column(Integer, primary_key=True)
174+
coder_id = Column(Integer, ForeignKey('user.id'))
175+
event_id = Column(Integer, ForeignKey('event.id'))
176+
last_accessed = Column(DateTime)
177+
178+
UniqueConstraint('coder_id', 'event_id', name = 'unique1')
179+
180+
def __init__(self, coder_id, event_id):
181+
self.coder_id = coder_id
182+
self.event_id = event_id
183+
self.last_accessed = dt.datetime.now(tz = central).replace(tzinfo = None)
184+
185+
def __repr__(self):
186+
return '<RecentEvent %r (%r)>' % (self.event_id, self.last_accessed)
187+
188+
189+
class RecentSearch(Base):
190+
__tablename__ = 'recent_search'
191+
id = Column(Integer, primary_key=True)
192+
coder_id = Column(Integer, ForeignKey('user.id'))
193+
field = Column(Text)
194+
comparison = Column(Text)
195+
value = Column(Text)
196+
last_accessed = Column(DateTime)
197+
198+
UniqueConstraint('coder_id', 'field', 'comparison', 'value', name = 'unique1')
199+
200+
def __init__(self, coder_id, field, comparison, value):
201+
self.coder_id = coder_id
202+
self.field = field
203+
self.comparison = comparison
204+
self.value = value
205+
self.last_accessed = dt.datetime.now(tz = central).replace(tzinfo = None)
206+
207+
def __repr__(self):
208+
return '<RecentSearch %r-%r-%r (%r)>' % \
209+
(self.field, self.comparison, self.value, self.last_accessed)
210+
211+
32212
class CodeFirstPass(Base):
33213
__tablename__ = 'coder_first_pass'
34214
id = Column(Integer, primary_key=True)
@@ -116,7 +296,7 @@ class ArticleQueue(Base):
116296
id = Column(Integer, primary_key=True)
117297
article_id = Column(Integer, ForeignKey('article_metadata.id'), nullable = False)
118298
coder_id = Column(Integer, ForeignKey('user.id'), nullable = False)
119-
coded_dt = Column(DateTime)
299+
coded_dt = Column(DateTime)
120300

121301
UniqueConstraint('article_id', 'coder_id', name = 'unique1')
122302

@@ -219,11 +399,11 @@ def __init__(self, username, password, authlevel):
219399
self.password = password
220400
self.authlevel = authlevel
221401

222-
def get_id(self):
223-
try:
224-
return unicode(self.id) # python 2
225-
except NameError:
226-
return str(self.id) # python 3
402+
# def get_id(self):
403+
# try:
404+
# return unicode(self.id) # python 2
405+
# except NameError:
406+
# return str(self.id) # python 3
227407

228408
def __repr__(self):
229409
return '<Coder %r>' % (self.username)

‎mpeds_coder.py

+816-141
Large diffs are not rendered by default.

‎scripts/add_test_entries.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
2+
3+
from context import config, database
4+
from models import CanonicalEvent, CanonicalEventLink, CodeEventCreator, RecentEvent, RecentCanonicalEvent
5+
import random
6+
7+
CODER_ID_ADJ1 = 43
8+
9+
def add_canonical_event():
10+
""" Adds a canonical event and linkages to two candidate events."""
11+
12+
## Add event
13+
ce = database.db_session.query(CanonicalEvent).filter(CanonicalEvent.key == 'Milo_Chicago_2016').first()
14+
15+
if not ce:
16+
ce = CanonicalEvent(coder_id = CODER_ID_ADJ1,
17+
key = 'Milo_Chicago_2016',
18+
notes = 'This is the main event for the protest against Milo in Chicago in 2016. This is filler text to try to break this part of it. This is filler text to try to break this part of it. This is filler text to try to break this part of it. This is filler text to try to break this part of it.',
19+
status = 'In progress')
20+
21+
## commit this first so we can get the ID
22+
database.db_session.add(ce)
23+
database.db_session.commit()
24+
25+
## get two of the candidate events and store in tables
26+
cand_events = {6032: {}, 21646: {}}
27+
28+
for event_id in cand_events.keys():
29+
for record in database.db_session.query(CodeEventCreator).filter(CodeEventCreator.event_id == event_id).all():
30+
## put in a list if it a single valued variable
31+
if record.variable not in cand_events[event_id]:
32+
if record.variable not in config.SINGLE_VALUE_VARS:
33+
cand_events[event_id][record.variable] = []
34+
35+
## store the ID as the single variable if SV
36+
if record.variable in config.SINGLE_VALUE_VARS:
37+
cand_events[event_id][record.variable] = record.id
38+
else: ## else, push into the list
39+
cand_events[event_id][record.variable].append(record.id)
40+
41+
## new list for links
42+
cels = []
43+
44+
## randomly put in single-value'd elements into the canonical event
45+
for sv in config.SINGLE_VALUE_VARS:
46+
event_id = random.choice(list(cand_events.keys()))
47+
48+
## skip if not in cand_events
49+
if sv not in cand_events[event_id]:
50+
continue
51+
52+
## add the new CELink
53+
cels.append(CanonicalEventLink(
54+
coder_id = CODER_ID_ADJ1,
55+
canonical_id = ce.id,
56+
cec_id = cand_events[event_id][sv]
57+
))
58+
59+
## just add in the rest of the data
60+
for event_id in cand_events.keys():
61+
for variable in cand_events[event_id].keys():
62+
63+
## skip because we're ignoring single values
64+
if variable in config.SINGLE_VALUE_VARS:
65+
continue
66+
67+
for value in cand_events[event_id][variable]:
68+
cels.append(CanonicalEventLink(
69+
coder_id = CODER_ID_ADJ1,
70+
canonical_id = ce.id,
71+
cec_id = value
72+
))
73+
74+
database.db_session.add_all(cels)
75+
database.db_session.commit()
76+
77+
78+
def add_recent_events():
79+
""" Add two recent candidate events. """
80+
database.db_session.add_all([
81+
RecentEvent(CODER_ID_ADJ1, 6032),
82+
RecentEvent(CODER_ID_ADJ1, 21646)
83+
])
84+
database.db_session.commit()
85+
86+
87+
def add_recent_canonical_events():
88+
""" Add example canonical event to recent canonical events. """
89+
ce = database.db_session.query(CanonicalEvent).filter(CanonicalEvent.key == 'Milo_Chicago_2016').first()
90+
91+
database.db_session.add(RecentCanonicalEvent(CODER_ID_ADJ1, ce.id))
92+
database.db_session.commit()
93+
94+
if __name__ == "__main__":
95+
#add_recent_events()
96+
#add_recent_canonical_events()
97+
pass

‎scripts/cec_long_to_wide.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
##
2+
## Generate a wide-to-long CEC table from a long-to-wide CEC table.
3+
##
4+
5+
import pandas as pd
6+
import sqlalchemy
7+
8+
import os
9+
import sys
10+
import yaml
11+
12+
import datetime as dt
13+
14+
sys.path.insert(0, os.path.join(os.path.abspath('.'), 'scripts'))
15+
16+
from context import config
17+
18+
## MySQL setup
19+
mysql_engine = sqlalchemy.create_engine(
20+
'mysql://%s:%s@localhost/%s?unix_socket=%s&charset=%s' %
21+
(config.MYSQL_USER,
22+
config.MYSQL_PASS,
23+
config.MYSQL_DB,
24+
config.MYSQL_SOCK,
25+
'utf8mb4'), convert_unicode=True)
26+
27+
## get the users to skip
28+
non_users = ['test1', 'admin', 'tina', 'alex', 'ellen', 'ishita', 'andrea', 'karishma', 'adj1']
29+
30+
## get the disqualifying information rows
31+
disqualifying_variables = yaml.load(
32+
open(os.path.join(os.path.abspath('..'), 'yes-no.yaml'), 'r'),
33+
Loader = yaml.BaseLoader)
34+
disqualifying_variables = [x[0] for x in disqualifying_variables['Disqualifying information']]
35+
36+
query = """SELECT
37+
event_id,
38+
u.username coder_id,
39+
variable,
40+
value,
41+
cec.text,
42+
am.id article_id,
43+
am.pub_date,
44+
am.publication,
45+
am.title
46+
FROM coder_event_creator cec
47+
LEFT JOIN article_metadata am ON (cec.article_id = am.id)
48+
LEFT JOIN user u ON (cec.coder_id = u.id)"""
49+
50+
## get the query
51+
df_long = pd.read_sql(query, con = mysql_engine)
52+
53+
## there should not be duplicates but here we are
54+
df_long = df_long.drop_duplicates()
55+
56+
## remove test users
57+
df_long = df_long[~df_long['coder_id'].isin(non_users)]
58+
59+
## get disqualified events and remove
60+
disqualified_events = df_long[df_long['variable'].isin(disqualifying_variables)].event_id.unique()
61+
df_long = df_long[~df_long['event_id'].isin(disqualified_events)]
62+
63+
## move text field into value if not null
64+
df_long['value'] = df_long.apply(lambda x: x['text'] if x['text'] is not None else x['value'], axis = 1)
65+
66+
## pivot, join variables with multiple values with ;
67+
indexes = ['event_id', 'coder_id', 'article_id', 'publication', 'pub_date', 'title']
68+
df_wide = pd.pivot_table(data = df_long,
69+
index = indexes,
70+
columns = 'variable',
71+
values = 'value',
72+
aggfunc = lambda x: ';'.join(x))
73+
74+
df_wide.to_csv('../exports/pivoted-events_{}.csv'.format(dt.datetime.now().strftime('%Y-%m-%d')))

‎scripts/context.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@
66
import config
77
import database
88
import models
9-
from modules import export
10-
9+
from modules import export

‎scripts/generate_event_metadata.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import pandas as pd
2+
import numpy as np
3+
import sqlalchemy
4+
5+
import os
6+
import sys
7+
import yaml
8+
9+
sys.path.insert(0, os.path.join(os.path.abspath('.'), 'scripts'))
10+
11+
from context import config
12+
13+
## MySQL setup
14+
mysql_engine = sqlalchemy.create_engine(
15+
'mysql://%s:%s@localhost/%s?unix_socket=%s&charset=%s' %
16+
(config.MYSQL_USER,
17+
config.MYSQL_PASS,
18+
config.MYSQL_DB,
19+
config.MYSQL_SOCK,
20+
'utf8mb4'), convert_unicode=True)
21+
22+
## get the users to skip
23+
non_users = ['test1', 'admin', 'tina', 'alex', 'ellen', 'ishita', 'andrea', 'karishma']
24+
25+
## get the disqualifying information rows
26+
disqualifying_variables = yaml.load(
27+
open(os.path.join(os.path.abspath('..'), 'yes-no.yaml'), 'r'),
28+
Loader = yaml.BaseLoader)
29+
disqualifying_variables = [x[0] for x in disqualifying_variables['Disqualifying information']]
30+
31+
query = """SELECT
32+
cec.event_id,
33+
u.username AS coder_id,
34+
cec.variable,
35+
cec.value,
36+
cec.text,
37+
am.id AS article_id,
38+
am.pub_date,
39+
am.publication,
40+
am.title,
41+
cec2.form AS form
42+
FROM coder_event_creator cec
43+
LEFT JOIN article_metadata am ON (cec.article_id = am.id)
44+
LEFT JOIN user u ON (cec.coder_id = u.id)
45+
LEFT JOIN (SELECT
46+
event_id, GROUP_CONCAT(value SEPARATOR ';') AS form
47+
FROM coder_event_creator
48+
WHERE variable = 'form'
49+
GROUP BY 1
50+
) cec2 ON (cec.event_id = cec2.event_id)
51+
"""
52+
53+
## get the query
54+
df_long = pd.read_sql(query, con = mysql_engine)
55+
56+
## there should not be duplicates but here we are
57+
df_long = df_long.drop_duplicates()
58+
59+
## remove test users
60+
df_long = df_long[~df_long['coder_id'].isin(non_users)]
61+
62+
## get disqualified events and remove
63+
disqualified_events = df_long[df_long['variable'].isin(disqualifying_variables)].event_id.unique()
64+
df_long = df_long[~df_long['event_id'].isin(disqualified_events)]
65+
66+
## move text field into value if not null
67+
df_long['value'] = df_long.apply(lambda x: x['text'] if x['text'] is not None else x['value'], axis = 1)
68+
69+
## pivot
70+
columns = ['article-desc', 'desc', 'location', 'start-date']
71+
indexes = ['event_id', 'coder_id', 'article_id', 'publication', 'pub_date', 'title', 'form']
72+
df_wide = df_long[df_long['variable'].isin(columns)].\
73+
pivot(index = indexes, columns = 'variable', values = 'value')
74+
75+
## rename a few things to be MySQL and SQLAlchemy friendly
76+
df_wide = df_wide.rename(columns = {'article-desc': 'article_desc', 'start-date': 'start_date'})
77+
78+
## reset indexes
79+
df_wide = df_wide.reset_index()
80+
81+
## replace empty values with NaN
82+
df_wide[df_wide == ''] = np.nan
83+
84+
## upload to MySQL
85+
df_wide.to_sql(name = 'event_metadata',
86+
con = mysql_engine,
87+
if_exists= 'replace',
88+
index = True,
89+
index_label = 'id',
90+
dtype = {
91+
'id': sqlalchemy.types.Integer(),
92+
'coder_id': sqlalchemy.types.Text(),
93+
'event_id': sqlalchemy.types.Integer(),
94+
'article_id': sqlalchemy.types.Integer(),
95+
'article_desc': sqlalchemy.types.UnicodeText(),
96+
'desc': sqlalchemy.types.UnicodeText(),
97+
'location': sqlalchemy.types.Text(),
98+
'start_date': sqlalchemy.types.Date(),
99+
'publication': sqlalchemy.types.Text(),
100+
'pub_date': sqlalchemy.types.Date(),
101+
'title': sqlalchemy.types.Text(),
102+
'form': sqlalchemy.types.Text()
103+
})

‎static/adj.js

+825
Large diffs are not rendered by default.

‎static/example-event-hierarchy.png

37 KB
Loading

‎static/style.css

+146-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
22
margin-bottom: 1em; background: #fafafa; }
3-
.flash { background: #CEE5F5; padding: 0.5em;
4-
border: 1px solid #AACBE2; }
5-
.error { background: #F0D6D6; padding: 0.5em; }
3+
.flash {
4+
padding: 1em;
5+
position: absolute;
6+
z-index: 10;
7+
top: 0;
8+
right: 0;
9+
border: 1px solid #ccc;
10+
}
611

712
.control-panel {
813
max-height: calc(100vh - 80px);
@@ -41,12 +46,10 @@ label, select, option, input {
4146
}
4247

4348
.hl-1 {
44-
# background-color: #FFF2CC;
4549
background-color: #CCF5FF;
4650
}
4751

4852
.hl-2 {
49-
# background-color: #FFD966;
5053
background-color: #66E0FF;
5154
}
5255

@@ -69,12 +72,10 @@ label, select, option, input {
6972
.hl-19,
7073
.hl-20,
7174
.hl-21 {
72-
# background-color: #BF9000;
7375
background-color: #00A3CC;
7476
}
7577

7678
.options {
77-
#font-size: 0.9em;
7879
color: #777;
7980
padding-left: 6px;
8081
display: none;
@@ -232,14 +233,10 @@ div.event-item {
232233
border-radius: 5px;
233234
}
234235

235-
.event-desc:hover {
236+
.event-desc.completed {
236237
background-color: #E2E3E5;
237238
}
238239

239-
.event-desc.selected {
240-
background-color: #CCE5FF;
241-
}
242-
243240
div.icons {
244241
float: left;
245242
width: 30px;
@@ -372,3 +369,140 @@ footer {
372369
font-size: 0.8em;
373370
}
374371

372+
/*
373+
Adjudication variables
374+
*/
375+
.adj-variable-group, .event-group {
376+
padding: 2px 0;
377+
}
378+
379+
.adj-variable-group select,
380+
.adj-variable-group input {
381+
font-size: 0.9em;;
382+
}
383+
384+
.event-group {
385+
max-height: 640px;
386+
overflow-y: scroll;
387+
}
388+
389+
.adj-variable-group .form-row div {
390+
margin: 0 -10px;
391+
}
392+
393+
.adj-pane {
394+
/* padding-right: 2px; */
395+
border-right: 1px solid #CCC;
396+
}
397+
398+
.info-block {
399+
margin: 10px;
400+
padding: 20px;
401+
text-align: center;
402+
vertical-align: middle;
403+
border: #36708D 2px dashed;
404+
font-size: 1.3em;
405+
}
406+
407+
span.adj-variable:hover {
408+
background-color: #CCC;
409+
}
410+
411+
div.event-group div.event-desc {
412+
height: 150px;
413+
width: auto;
414+
overflow-y: scroll;
415+
overflow-x: hidden;
416+
font-size: 0.85em;
417+
padding-left: 4px;
418+
margin: 4px 0;
419+
}
420+
421+
#adj-subtabselecter {
422+
margin-top: 4px;
423+
}
424+
425+
#expanded-event-view-metadata {
426+
border-bottom: 1px solid #888;
427+
/* overflow-x: auto;
428+
white-space: nowrap; */
429+
}
430+
431+
.expanded-event-view {
432+
height: 640px;
433+
overflow-y: scroll;
434+
overflow-x: hidden;
435+
/* overflow-x: auto;
436+
white-space: nowrap; */
437+
}
438+
439+
div.expanded-event-variable-name {
440+
font-size: 0.8em;
441+
}
442+
443+
div.expanded-event-variable-metadata {
444+
cursor: not-allowed;
445+
color: #888;
446+
}
447+
448+
div.expanded-event-variable {
449+
border: 1px dashed #AAA;
450+
border-radius: 5px;
451+
margin: 4px -30px 4px 0;
452+
padding: 2px;
453+
font-size: 0.8em;
454+
background-color: #EEE;
455+
width: 100%;
456+
display: block;
457+
max-height: 100px;
458+
overflow-y: scroll;
459+
}
460+
461+
div.maximize {
462+
max-height: none !important;
463+
}
464+
465+
div.canonical {
466+
margin-left: 5px;
467+
background-color: #CCE5FF;
468+
}
469+
470+
a#new-canonical {
471+
font-size: 2em;
472+
position: absolute;
473+
z-index: 5;
474+
top: 0;
475+
right: 0;
476+
}
477+
478+
div.canonical-dummy {
479+
margin-left: 5px;
480+
background-color: #E2EFDA;
481+
}
482+
483+
.data-buttons {
484+
float: right;
485+
}
486+
487+
div.expanded-event-variable-group {
488+
margin-bottom: 4px;
489+
border-bottom: 1px solid #CCC;
490+
}
491+
492+
div.expanded-event-variable-col {
493+
padding-right: 0 !important;
494+
}
495+
496+
div.action-buttons {
497+
font-size: 1.2em;
498+
}
499+
500+
div.action-buttons a {
501+
padding-right: 4px;
502+
}
503+
504+
p.event-groupers a {
505+
padding: 2px;
506+
margin-right: 4px;
507+
border: 1px solid #AAA;
508+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<div class="event-group" id="canonical-search-block">
2+
{% for e in events %}
3+
<div class="event-desc canonical-search" id="canonical-event_{{ e.id }}"
4+
data-event="{{ e.id }}"
5+
data-key="{{ e.key }}">
6+
<div class="row">
7+
<div class="col-sm-9">
8+
<b>key:</b> {{ e.key }}</br>
9+
<b>event_id:</b> {{ e.id }}<br/>
10+
<b>coder:</b> {{ e.coder_id }} <br/>
11+
<b>last updated:</b> {{ e.last_updated }}
12+
</div>
13+
<div class="col-sm-3">
14+
<a href="#" class="canonical-makeactive">
15+
<b>Add to grid <span class="glyphicon glyphicon-export"></span></b>
16+
</a>
17+
<b class="canonical-isactive text-muted" style="display:none;">In the grid</b>
18+
</div>
19+
</div>
20+
<div class="row">
21+
<div class="col-sm-12">
22+
<b>desc:</b> {{ e.description | safe }}<br/>
23+
<b>notes:</b> {{ e.notes | safe }}
24+
</div>
25+
</div>
26+
</div>
27+
{% endfor %}
28+
</div>

‎templates/adj-filter.html

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{% for i in range(4) %}
2+
<div class="row form-row">
3+
<div class="col-sm-4">
4+
<select class="form-control"
5+
name="adj_filter_field_{{ i }}"
6+
id="adj_filter_field_{{ i }}">
7+
<option value="">---</option>
8+
{% for field in filter_fields %}
9+
<option>{{ field }}</option>
10+
{% endfor %}
11+
</select>
12+
</div>
13+
<div class="col-sm-4">
14+
<select class="form-control"
15+
name="adj_filter_compare_{{ i }}"
16+
id="adj_filter_compare_{{ i }}">
17+
<option value="">---</option>
18+
<option value="eq">equals</option>
19+
<option value="ne">not equal to</option>
20+
<option value="gt">is greater than</option>
21+
<option value="ge">is greater than or equal to</option>
22+
<option value="lt">is less than</option>
23+
<option value="le">is less than or equal to</option>
24+
<option value="contains">contains</option>
25+
<option value="starts">starts with</option>
26+
<option value="ends">ends with</option>
27+
</select>
28+
</div>
29+
<div class="col-sm-3">
30+
<input class="form-control"
31+
name="adj_filter_value_{{ i }}"
32+
id="adj_filter_value_{{ i }}"
33+
type="text"
34+
placeholder = "Value..."/>
35+
</div>
36+
<div class="col-sm-1">
37+
<a id="adj_filter_clear_button_{{ i }}" class="btn clear-values glyphicon glyphicon-remove" title="Clear values"></a>
38+
</div>
39+
</div>
40+
{% endfor %}

‎templates/adj-grid.html

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{% set var_adds = ['start-date', 'end-date', 'location', 'form', 'issue', 'racial-issue', 'target'] -%}
2+
{% if canonical_event is none and (cand_events is none or cand_events.keys()|length == 0) %}
3+
<p class="info-block bg-info text-info">
4+
Get started by selected or adding events.
5+
</p>
6+
{% else %}
7+
<div id="expanded-event-view-metadata" class="sticky-top">
8+
<div class="row expanded-event-variable-group">
9+
<div class="col-sm-1 expanded-event-variable-name">
10+
metadata
11+
<a href="#" id="hide-metadata" title="Hide metadata">
12+
<span class="glyphicon glyphicon-chevron-up"></span>
13+
</a>
14+
<a href="#" id="show-metadata" style="display:none;" title="Show metadata">
15+
<span class="glyphicon glyphicon-chevron-down"></span>
16+
</a>
17+
</div>
18+
{% for event_id in cand_events.keys()|sort %}
19+
<!-- Candidate Event Metadata -->
20+
<div class="col-sm-2 expanded-event-variable-col candidate-event"
21+
id="candidate-event-metadata_{{ event_id }}"
22+
data-event="{{ event_id }}"
23+
data-article="{{ cand_events[event_id]['metadata']['article_id'] }}"
24+
data-coder="{{ cand_events[event_id]['metadata']['coder_id'] }}">
25+
<label>Event {{ event_id }}<br/>
26+
<div class="action-buttons">
27+
{% set article_id = cand_events[event_id]['metadata']['article_id'] -%}
28+
{% if article_id in links %}
29+
<a class="glyphicon glyphicon-link text-danger remove-link"
30+
title="Remove link to canonical event"
31+
data-article="{{ article_id if article_id in links else '' }}"></a>
32+
{% else %}
33+
<a class="glyphicon glyphicon-link add-link" title="Link to canonical event" ></a>
34+
{% endif %}
35+
36+
{% if event_id in flags.keys() and flags[event_id] == 'for-review' %}
37+
<a class="glyphicon glyphicon-flag remove-flag text-danger" title="Remove flag"></a>
38+
{% else %}
39+
<a class="glyphicon glyphicon-flag add-flag" title="Flag for later"></a>
40+
{% endif %}
41+
42+
{% if event_id in flags.keys() and flags[event_id] == 'completed' %}
43+
<a class="glyphicon glyphicon-check remove-completed text-danger" title="Remove completed"></a>
44+
{% else %}
45+
<a class="glyphicon glyphicon-check add-completed" title="Mark completed"></a>
46+
{% endif %}
47+
48+
<a class="glyphicon glyphicon-remove-sign remove-candidate" title="Remove from grid"></a>
49+
</div>
50+
</label>
51+
<div class="expanded-event-variable expanded-event-variable-metadata"
52+
id="expanded-event-metadata_{{ event_id }}">
53+
<b>ttl:</b> {{ cand_events[event_id]['metadata']['title'] | safe }}<br/>
54+
<b>pub:</b> {{ cand_events[event_id]['metadata']['publication'] | safe }}<br/>
55+
<b>p_d:</b> {{ cand_events[event_id]['metadata']['pub_date'] }}<br/>
56+
<b>aid:</b>
57+
<a href="{{ url_for('eventCreator', aid = cand_events[event_id]['metadata']['article_id']) }}"
58+
target="_blank">
59+
{{ cand_events[event_id]['metadata']['article_id'] }}
60+
</a><br/>
61+
<b>cod:</b> {{ cand_events[event_id]['metadata']['coder_id'] }} <br/>
62+
</div>
63+
</div>
64+
{% endfor %}
65+
<!-- Canonical Event Metadata -->
66+
<div class="col-sm-2 expanded-event-variable-col canonical-event canonical-event-metadata"
67+
id="canonical-event-metadata_{{ canonical_event['id'] }}"
68+
data-key="{{ canonical_event['key'] }}">
69+
{% if canonical_event %}
70+
<label>Canonical Event<br/>
71+
<div class="action-buttons">
72+
<a class="glyphicon glyphicon-pencil edit-canonical" href="#" title="Edit canonical metadata"></a>
73+
<a class="glyphicon glyphicon-trash" href="#" title="Delete canonical event"></a>
74+
<a class="glyphicon glyphicon-remove-sign" href="#" title="Remove from grid"></a>
75+
</div>
76+
</label>
77+
{% for var in ['key', 'notes'] %}
78+
<div class="expanded-event-variable canonical" id="expanded-event-canonical-{{ var }}">
79+
<b>{{ var|title }}: </b> {{ canonical_event[var]|safe }}<br />
80+
</div>
81+
{% endfor %}
82+
{% else %}
83+
<label>Canonical Event</label>
84+
<div class="expanded-event-variable canonical">
85+
<i>Add or select a canonical event to start.</i>
86+
</div>
87+
{% endif %}
88+
</div>
89+
</div>
90+
</div>
91+
<div class="expanded-event-view">
92+
{% for var in grid_vars %}
93+
<div class="row expanded-event-variable-group">
94+
<div class="col-sm-1 expanded-event-variable-name" data-var="{{ var }}">{{ var }}<br/>
95+
{% if var in var_adds %}
96+
<a class="glyphicon glyphicon-plus add-dummy" title="Add new value for this field" href="#"></a>
97+
{% elif var in ['article-desc', 'desc'] %}
98+
<a href="#" id="collapse-{{ var }}" title="Minimize {{ var }}">
99+
<span class="glyphicon glyphicon-resize-small"></span>
100+
</a>
101+
<a href="#" id="expand-{{ var }}" title="Expand {{ var }}" style="display:none;">
102+
<span class="glyphicon glyphicon-resize-full"></span>
103+
</a>
104+
{% endif %}
105+
</div>
106+
<!-- Candidate Event Fields -->
107+
{% for event_id in cand_events.keys()|sort %}
108+
<div class="col-sm-2 expanded-event-variable-col">
109+
{% if cand_events[event_id][var]|length > 0 %}
110+
{% for value, cec_id, timestamp in cand_events[event_id][var] %}
111+
<div class="expanded-event-variable {{ 'maximize' if var in ['article-desc', 'desc'] else ''}}"
112+
data-var="{{ var }}">
113+
<div class="sticky-top data-buttons">
114+
{% if var == 'article-desc' %} <!-- no-op -->
115+
{% elif var == 'desc' %}
116+
<a class="glyphicon glyphicon-duplicate select-text" href="#" title="Select all text"></a>
117+
{% else %}
118+
<a class="glyphicon glyphicon-plus add-val"
119+
title="Add to canonical event"
120+
data-key="{{ cec_id }}"></a>
121+
{% endif %}
122+
<!-- <a class="glyphicon glyphicon-info-sign"
123+
title="Edited on {{ timestamp }}"></a> -->
124+
</div>
125+
<div class="expanded-event-value">{{ value | safe }}</div>
126+
</div>
127+
{% endfor %}
128+
{% else %}
129+
<div class="expanded-event-variable text-muted none">(None)</div>
130+
{% endif %}
131+
</div>
132+
{% endfor %}
133+
<!-- Canonical Event Fields -->
134+
<div class="col-sm-2 expanded-event-variable-col canonical-event" id="canonical-event_{{ var }}">
135+
{% if canonical_event %}
136+
{% if canonical_event[var]|length > 0 %}
137+
{% for cel_id, value, timestamp, _, is_dummy in canonical_event[var] %}
138+
{% include 'canonical-cell.html' %}
139+
{% endfor %}
140+
{% elif var == 'article-desc' %}
141+
<div class="expanded-event-variable text-muted canonical none">N/A</div>
142+
{% elif var == 'desc' %}
143+
{% set value = canonical_event['description'] %}
144+
{% include 'canonical-cell.html' %}
145+
{% else %}
146+
<div class="expanded-event-variable text-muted canonical none">(None)</div>
147+
{% endif %}
148+
{% endif %}
149+
</div>
150+
</div>
151+
{% endfor %}
152+
</div>
153+
{% endif %}

‎templates/adj-search-block.html

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<div class="event-group">
2+
{% for e in events %}
3+
{% if flags[e.event_id] == 'for-review' %}
4+
{%- set event_class = 'bg-danger' %}
5+
{% elif flags[e.event_id] == 'completed' %}
6+
{%- set event_class = 'completed' %}
7+
{% else %}
8+
{%- set event_class = '' %}
9+
{% endif %}
10+
<div class="event-desc candidate-search {{ event_class }}" id="cand-event_{{ e.event_id }}" data-event="{{ e.event_id }}">
11+
<div class="row">
12+
<div class="col-sm-6">
13+
<b>st_d:</b> {{ e.start_date }}</br>
14+
<b>loc:</b> {{ e.location | safe }}<br/>
15+
<b>ttl:</b> {{ e.title[0:30] }}{{ '...' if e.title > 30 else '' }} <br/>
16+
<b>form:</b> {{ e.form | safe }}<br />
17+
</div>
18+
<div class="col-sm-3">
19+
<b>eid:</b> {{ e.event_id }}<br/>
20+
<b>aid:</b>
21+
<a href="{{ url_for('eventCreator', aid = e.article_id) }}"
22+
target="_blank"
23+
title="{{ e.publication }} ({{ e.pub_date }}) --- {{ e.article_desc|safe }}">{{e.article_id}}</a><br/>
24+
<b>cod:</b> {{ e.coder_id }} <br/>
25+
</div>
26+
<div class="col-sm-3">
27+
{% if e.event_id in flags %}
28+
<p class="flags">
29+
{% if flags[e.event_id] == 'for-review' %}
30+
<b class="text-danger">for-review</b>
31+
{% elif flags[e.event_id] == 'completed' %}
32+
<b class="text-muted">completed</b>
33+
{% endif %}
34+
</p>
35+
{% endif %}
36+
<a href="#" class="cand-makeactive">
37+
<b>Add to grid <span class="glyphicon glyphicon-export"></span></b>
38+
</a>
39+
<span class="cand-isactive text-muted" style="display:none;">In the grid</span>
40+
{% if e.event_id not in flags %}
41+
<p class="event-groupers">
42+
{% for class in ['info', 'warning', 'success'] %}
43+
<a class="bg-{{ class }} glyphicon glyphicon-unchecked" title="Add to group"></a>
44+
{% endfor %}
45+
</p>
46+
{% endif %}
47+
</div>
48+
</div>
49+
<div class="row">
50+
<div class="col-sm-12">
51+
<b>desc:</b> {{ e.desc | safe }}<br/>
52+
</div>
53+
</div>
54+
</div>
55+
{% endfor %}
56+
</div>
57+
<!-- <nav aria-label="cand-event-pagination">
58+
<ul class="pagination">
59+
<li class="page-item">
60+
<a class="page-link" href="#" aria-label="Previous">
61+
<span aria-hidden="true">&laquo;</span>
62+
<span class="sr-only">Previous</span>
63+
</a>
64+
</li>
65+
<li class="page-item"><a class="page-link" href="#">1</a></li>
66+
<li class="page-item"><a class="page-link" href="#">2</a></li>
67+
<li class="page-item"><a class="page-link" href="#">3</a></li>
68+
<li class="page-item">
69+
<a class="page-link" href="#" aria-label="Next">
70+
<span aria-hidden="true">&raquo;</span>
71+
<span class="sr-only">Next</span>
72+
</a>
73+
</li>
74+
</ul>
75+
</nav> -->

‎templates/adj-sort.html

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% for i in range(4) %}
2+
<div class="row form-row">
3+
<div class="col-sm-4">
4+
<select class="form-control"
5+
name="adj_sort_field_{{ i }}"
6+
id="adj_sort_field_{{ i }}">
7+
<option value="">---</option>
8+
{% for field in filter_fields %}
9+
<option>{{ field }}</option>
10+
{% endfor %}
11+
</select>
12+
</div>
13+
<div class="col-sm-4">
14+
<select class="form-control"
15+
name="adj_sort_order_{{ i }}"
16+
id="adj_sort_order_{{ i }}">
17+
<option value="">---</option>
18+
<option value="asc">ascending</option>
19+
<option value="desc">descending</option>
20+
</select>
21+
</div>
22+
<div class="col-sm-1">
23+
<a id="adj_sort_clear_button_{{ i }}" class="btn clear-values glyphicon glyphicon-remove" title="Clear values"></a>
24+
</div>
25+
</div>
26+
{% endfor %}

‎templates/adj.html

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
{% extends "layout.html" %}
2+
{% block body %}
3+
<script src="{{ url_for('static', filename='shared.js') }}" type="text/javascript"></script>
4+
<script src="{{ url_for('static', filename='adj.js') }}" type="text/javascript"></script>
5+
<div class="container-fluid">
6+
<div class="row">
7+
<!-- flash -->
8+
<div class="col-sm-12"><div class="flash" style="display:none;"></div></div>
9+
<!-- Modal -->
10+
<div class="modal fade" id="modal-container" tabindex="-1" role="dialog" aria-labelledby="FormModal" aria-hidden="true">
11+
<div class="modal-dialog" role="document">
12+
<div class="modal-content">
13+
<!-- Content goes here -->
14+
</div>
15+
</div>
16+
</div>
17+
</div>
18+
<div class="row">
19+
<div class="col-sm-5 adj-pane" id="adj-pane-cand-events">
20+
<a id="cand-events-hide" href="#">&laquo; Hide Search</a><br/>
21+
<a href="{{ url_for('adj',
22+
cand_events = '2544,2545,2572,2708',
23+
adj_search_input = 'Amherst',
24+
adj_filter_field_0 = 'start_date',
25+
adj_filter_compare_0 = 'ge',
26+
adj_filter_value_0 = '2015-11-01',
27+
adj_filter_field_1 = 'start_date',
28+
adj_filter_compare_1 = 'lt',
29+
adj_filter_value_1 = '2015-11-30',
30+
adj_sort_field_0 = 'start_date',
31+
adj_sort_order_0 = 'asc') }}">
32+
Example page
33+
</a>
34+
<h4>Event Selection</h4>
35+
<ul class="nav nav-tabs" id="adj-tabselecter">
36+
<li class="tablinks active" id="search_button">
37+
<a href="#">Search</a>
38+
</li>
39+
<li class="tablinks" id="cand_button">
40+
<a href="#" id="cand_button-link">Candidate Events</a>
41+
</li>
42+
<li class="tablinks" id="canonical_button">
43+
<a href="#">Canonical Events</a>
44+
</li>
45+
<li class="tablinks" id="relationships_button">
46+
<a href="#">Relationships</a>
47+
</li>
48+
</ul>
49+
<div class="tab-content">
50+
<div class="tab-pane" id="search_block">
51+
<div class="adj-variable-group">
52+
<h5>Search</h5>
53+
<form id="adj_search_form">
54+
<div class="row form-row">
55+
<div class="col-sm-9">
56+
<input class="form-control"
57+
name="adj_search_input"
58+
id="adj_search_input"
59+
type="text"
60+
placeholder = "Example: Mizzou OR Missouri, Quebec AND students"/>
61+
</div>
62+
<div class="col-sm-3">
63+
<button id="adj_search_button" class="btn btn-primary">Search</button>
64+
</div>
65+
</div>
66+
</form>
67+
</div>
68+
<div class="adj-variable-group">
69+
<h5>Filter</h5>
70+
<form id="adj_filter_form">
71+
{% include 'adj-filter.html' %}
72+
</form>
73+
</div>
74+
<div class="adj-variable-group">
75+
<h5>Sort</h5>
76+
<form id="adj_sort_form">
77+
{% include 'adj-sort.html' %}
78+
</form>
79+
</div>
80+
</div>
81+
<div class="tab-pane" id="cand_block">
82+
<ul class="nav nav-pills nav-justified">
83+
<li class="cand-subtablinks active nav-item" id="cand-search_button">
84+
<a href="#" id="cand-search-text">Search</a>
85+
</li>
86+
<li class="cand-subtablinks nav-item" id="cand-recent_button">
87+
<a href="#">Recent</a>
88+
</li>
89+
</ul>
90+
<div class="cand-subtab-pane" id="cand-search_block">
91+
{% set events = search_events %}
92+
{% include 'adj-search-block.html' %}
93+
</div>
94+
<div class="cand-subtab-pane" id="cand-recent_block" style="display:none;">
95+
{% set events = recent_events %}
96+
{% include 'adj-search-block.html' %}
97+
</div>
98+
</div>
99+
<div class="tab-pane" id="canonical_block">
100+
<ul class="nav nav-pills nav-justified">
101+
<li class="canonical-subtablinks active nav-item" id="canonical-search_button">
102+
<a href="#"">Search</a>
103+
</li>
104+
<li class="canonical-subtablinks nav-item" id="canonical-recent_button">
105+
<a href="#">Recent</a>
106+
</li>
107+
</ul>
108+
<div class="canonical-subtab-pane" id="canonical-search_block">
109+
<h5>Search</h5>
110+
<div class="row">
111+
<div class="form-group col-sm-9">
112+
<input class="form-control form-control-sm"
113+
name="canonical-search-term"
114+
id="canonical-search-term"
115+
type="text"
116+
placeholder="Search term (key or keyword in notes/description)"/>
117+
</div>
118+
<div class="col-sm-3">
119+
<button id="canonical-search-button" class="btn btn-primary btn-sm">Search</button>
120+
</div>
121+
</div>
122+
{% set events = [] %}
123+
{% include 'adj-canonical-search-block.html' %}
124+
</div>
125+
<div class="canonical-subtab-pane" id="canonical-recent_block" style="display:none;">
126+
{% set events = recent_canonical_events %}
127+
{% include 'adj-canonical-search-block.html' %}
128+
</div>
129+
</div>
130+
<div class="tab-pane" id="relationships_block">
131+
<h5>Add relationship</h5>
132+
<form>
133+
<div class="form-row">
134+
<div class="form-group col-sm-3">
135+
<input class="form-control form-control-sm"
136+
name="search-key-1"
137+
type="text"
138+
placeholder = "Key 1..."/>
139+
</div>
140+
<div class="form-group col-sm-3">
141+
<input class="form-control form-control-sm"
142+
name="search-key-2"
143+
type="text"
144+
placeholder = "Key 2..."/>
145+
</div>
146+
<div class="form-group col-sm-4">
147+
<select class="form-control form-control-sm" name="relationship-type">
148+
<option>Campaign</option>
149+
<option>Sub-event</option>
150+
<option>Counterprotest</option>
151+
<option>Solidarity</option>
152+
<option>Coordinated</option>
153+
</select>
154+
</div>
155+
<div class="form-group col-sm-2">
156+
<button type="submit" class="btn btm-primary">Add <span class="glyphicon glyphicon-plus"></span></button>
157+
</div>
158+
</div>
159+
</form>
160+
<hr />
161+
<div class="form-row">
162+
<h5>View hierarchy</h5> </br>
163+
<div class="form-group col-sm-8">
164+
<input class="form-control form-control-sm"
165+
name="view-key"
166+
type="text"
167+
placeholder = "Key..."/>
168+
</div>
169+
<p>
170+
<img src="static/example-event-hierarchy.png" width="50%"/>
171+
</p>
172+
</div>
173+
</div>
174+
</div>
175+
</div>
176+
<div class="col-sm-7 adj-pane" id="adj-pane-expanded-view">
177+
<a id="cand-events-show" style="display:none;" href="#">Show Search &raquo;</a>
178+
<a id="new-canonical"
179+
data-toggle="modal"
180+
data-target="my_modal"
181+
title="Add new canonical event"
182+
href="#"><span class="glyphicon glyphicon-plus-sign"></span></a>
183+
<div id="adj-grid">
184+
{% include 'adj-grid.html' %}
185+
</div>
186+
</div>
187+
</div>
188+
</div>
189+
{% endblock %}

‎templates/canonical-cell.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="expanded-event-variable {{ 'canonical-dummy' if is_dummy else 'canonical' }}"
2+
id="canonical-{{ var }}_{{ cel_id }}"
3+
data-var="{{ var }}"
4+
data-key="{{ cel_id }}">
5+
<div class="sticky-top data-buttons">
6+
{% if var != 'desc' %}
7+
<a class="glyphicon glyphicon-remove remove-canonical" title="Remove"></a>
8+
<a class="glyphicon glyphicon-info-sign"
9+
title="Added on {{ timestamp }}"></a>
10+
{% endif %}
11+
</div>
12+
{{ value | safe }}
13+
</div>

‎templates/code2.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<div class="container-fluid">
66
<div class="row">
77
<div class="col-xs-6">
8-
<div class="error" id="flash-error" style="display:none;"></div>
8+
<div class="alert-danger" id="flash-error" style="display:none;"></div>
99
<div class="article" id="article_{{ aid }}">
1010
{{ text|safe }}
1111
</div>

‎templates/index.html

+19-16
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,28 @@
44
<div class="row">
55
<div class="col-xs-8">
66
<h2>Welcome, {{ current_user.username|capitalize }}</h2>
7-
87
<div class="index">
9-
10-
<h4>Event creator</h4>
11-
{% if current_user.authlevel <= 2 %}
12-
<p><a href="{{ url_for('ecNext') }}">Event Coder Interface</a> | <a href="{{ url_for('userArticleList', pn = 'ec') }}">List</a></p>
8+
{% if current_user.authlevel == 1 %}
9+
<h4>Event creator</h4>
10+
<p><a href="{{ url_for('ecNext') }}">Event Coder Interface</a> | <a href="{{ url_for('userArticleList', pn = 'ec') }}">List</a></p>
11+
{% endif %}
12+
{% if current_user.authlevel == 2 %}
13+
<h4>Adjudication</h4>
14+
<p>
15+
<a href="{{ url_for('adj') }}">Adjudication Interface</a>
16+
</p>
17+
{% endif %}
1318
<!-- <h4>Second pass</h4>
1419
<p><a href="{{ url_for('code2Next') }}">Coding interface</a> | <a href="{{ url_for('userArticleList', pn = 2) }}">List</a> | <a href="{{ url_for('code2queue', sort = 'percent_yes', sort_dir = 'desc') }}">First pass summary</a></p> -->
15-
{% else %}
16-
<p><a href="{{ url_for('admin') }}">Admin Dashboard</a></p>
17-
<p>Publication report</p>
18-
<p>
19-
<a href="{{ url_for('publications', db = 'uwire') }}">UWIRE</a> |
20-
<a href="{{ url_for('publications', db = 'uwire2016-18') }}">UWIRE 2016-18</a> |
21-
<a href="{{ url_for('publications', db = 'canadian-papers') }}">Canadian</a> |
22-
<a href="{{ url_for('publications', db = 'hbcu-papers') }}">HBCUs</a>
23-
</p>
24-
25-
{% endif %}
20+
{% if current_user.authlevel == 3 %}
21+
<h4><a href="{{ url_for('admin') }}">Admin Dashboard</a></h4>
22+
<p>Publication reports:
23+
<a href="{{ url_for('publications', db = 'uwire') }}">UWIRE</a> |
24+
<a href="{{ url_for('publications', db = 'uwire2016-18') }}">UWIRE 2016-18</a> |
25+
<a href="{{ url_for('publications', db = 'canadian-papers') }}">Canadian</a> |
26+
<a href="{{ url_for('publications', db = 'hbcu-papers') }}">HBCUs</a>
27+
</p>
28+
{% endif %}
2629
<p><a href="{{ url_for('coderStats') }}">Coder stats</a></p>
2730
</div>
2831

‎templates/layout.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
<meta name="description" content="">
88
<meta name="author" content="Alex Hanna">
99

10-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='bower_components/bootstrap/dist/css/bootstrap.min.css') }}">
10+
<!-- TODO: Upgrade to Tempus Dominus picker. https://getdatepicker.com/ and source from CDN. -->
11+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script type="text/javascript" src="{{ url_for('static', filename='bower_components/moment/min/moment.min.js') }}"></script>
1112
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.css') }}" />
1213
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}" />
1314
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.png') }}" />
1415

15-
<script type="text/javascript" src="{{ url_for('static', filename='bower_components/jquery/dist/jquery.min.js') }}"></script>
16-
<script type="text/javascript" src="{{ url_for('static', filename='bower_components/bootstrap/dist/js/bootstrap.min.js') }}"></script>
17-
<script type="text/javascript" src="{{ url_for('static', filename='bower_components/moment/min/moment.min.js') }}"></script>
16+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
17+
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
1818
<script type="text/javascript" src="{{ url_for('static', filename='bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js') }}"></script>
1919

2020
<script type="text/javascript">

‎templates/list.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{% extends "layout.html" %}
22
{% block body %}
33
<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.11/css/jquery.dataTables.css">
4-
<div class="container">
4+
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.11/js/jquery.dataTables.js"></script>
5+
6+
<div class="container">
57
{% if current_user.authlevel > 2 %}
68
<h2>{{ username|capitalize }}'s {{ 'remaining' if is_coded == '0' else 'completed' }} queue</h2>
79
{% else %}
@@ -71,6 +73,4 @@ <h2>My coded articles</h2>
7173
</div>
7274
</div>
7375
</div>
74-
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.11/js/jquery.dataTables.js"></script>
75-
<script src="{{ url_for('static', filename='list.js') }}" type="text/javascript"></script>
7676
{% endblock %}

‎templates/login.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ <h2>Login</h2>
1212
<dd><input type="text" name="username">
1313
<dt>Password:
1414
<dd><input type="password" name="password">
15-
<dd><input type="submit" value="Login">
15+
<dd><input type="submit" name="login" value="Login">
1616
</dl>
1717
</form>
1818
</div>

‎templates/modal.html

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<div class="modal-header">
2+
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
3+
<h4 class="modal-title" id="l_my_modal">Add {{ variable|capitalize }}</h4>
4+
</div>
5+
<div class="modal-body">
6+
<div class="col-sm-12">
7+
<div id="modal-flash" class="alert" role="alert" style="display:none;"></div>
8+
</div>
9+
<form id="modal-form">
10+
<div class="form-group col-sm-12">
11+
{% if variable == 'canonical' %}
12+
<div class="form-row">
13+
<label for="canonical-event-key">Key</label>
14+
<input class="form-control form-control-sm"
15+
name="canonical-event-key"
16+
id="canonical-event-key"
17+
type="text"
18+
value="{{ ce.key if ce.key != None else '' }}"
19+
placeholder = "Canonical Event Key..."/>
20+
</div>
21+
<div class="form-row">
22+
<label for="canonical-event-desc">Description</label>
23+
<textarea class="form-control"
24+
name="canonical-event-desc"
25+
id="canonical-event-desc"
26+
placeholder = "Description...">{{ ce.description if ce.description != None else '' }}</textarea>
27+
</div>
28+
<div class="form-row">
29+
<label for="canonical-event-notes">Notes</label>
30+
<textarea class="form-control"
31+
name="canonical-event-notes"
32+
id="canonical-event-notes"
33+
placeholder = "Notes (optional)">{{ ce.notes if ce.notes != None else '' }}</textarea>
34+
</div>
35+
{% else %} <!-- Dummy variables-->
36+
<div class="form-row">
37+
<label for="article-id">Article ID</label>
38+
<select class="form-control form-control-sm" name="article-id">
39+
<option value="">Select article</option>
40+
{% for article_id in article_ids %}
41+
<option value="{{ article_id }}">{{ article_id }}</option>
42+
{% endfor %}
43+
</select>
44+
</div>
45+
{% if variable == 'start-date' or variable == 'end-date' %}
46+
<div class="form-row">
47+
<div class="input-group date">
48+
<label for="date-value">Date</label>
49+
<input class="form-control"
50+
type="text"
51+
id="date-value"
52+
name="value"
53+
value=""
54+
placeholder="Click calendar for date..." />
55+
<span class="input-group-addon">
56+
<span class="glyphicon glyphicon-calendar"></span>
57+
</span>
58+
</div>
59+
</div>
60+
{% elif variable in ['form', 'issue', 'racial-issue', 'target'] %}
61+
<div class="form-row">
62+
<div class="input-group">
63+
<label for="select-value">{{ variable | title }}</label>
64+
<select class="form-control form-control-sm" id="select-value" name="value">
65+
<option value="">---</option>
66+
{% for var in preset_vars[variable]|sort %}
67+
<option value="{{ var }}">{{ var }}</option>
68+
{% endfor %}
69+
</select>
70+
</div>
71+
</div>
72+
{% else %}
73+
<div class="form-row">
74+
<label for="variable-value">Value</label>
75+
<textarea class="form-control"
76+
name="value"
77+
id="variable-value"
78+
placeholder = "Enter text for {{ variable }}..."></textarea>
79+
</div>
80+
{% endif %}
81+
{% endif %}
82+
<input name="canonical-id" type="hidden" value="{{ ce.id }}" />
83+
</div>
84+
</form>
85+
</div>
86+
<div class="modal-footer">
87+
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
88+
<button type="button"
89+
class="btn btn-primary"
90+
name="modal-submit"
91+
id="modal-submit">Save</button>
92+
</div>

‎tests/context.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## From https://docs.python-guide.org/writing/structure/#test-suite
2+
import os
3+
import sys
4+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
5+
6+
import config
7+
import database

‎tests/test_add_event.py

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# -*- coding: utf-8 -*-
2+
import csv
3+
import os
4+
import random
5+
import unittest
6+
import yaml
7+
8+
from context import database
9+
from models import CodeEventCreator, Event
10+
11+
from sqlalchemy import desc
12+
13+
from selenium import webdriver
14+
from selenium.webdriver import Firefox, FirefoxOptions
15+
from selenium.webdriver.common.by import By
16+
from selenium.webdriver.common.keys import Keys
17+
18+
from selenium.webdriver.support import expected_conditions as EC
19+
from selenium.webdriver.support.ui import WebDriverWait
20+
21+
class EventAddTest(unittest.TestCase):
22+
driver = None
23+
24+
def setUp(self):
25+
print("SET-UP")
26+
## spin up new firefox session
27+
opts = FirefoxOptions()
28+
opts.add_argument("--headless")
29+
self.driver = Firefox(options=opts)
30+
31+
users = {}
32+
with open('credentials.csv', newline='') as csvfile:
33+
reader = csv.reader(csvfile, delimiter=',')
34+
for row in reader:
35+
users[row[0]] = row[1]
36+
37+
## Delete all the test entries
38+
q = database.db_session.query(CodeEventCreator).\
39+
filter(CodeEventCreator.coder_id == 2)
40+
q.delete()
41+
database.db_session.commit()
42+
43+
self.driver.get("http://cliff.ssc.wisc.edu/campus_protest_dev")
44+
45+
self.driver.find_element(By.NAME, 'username').send_keys('test1')
46+
self.driver.find_element(By.NAME, 'password').send_keys(users['test1'])
47+
self.driver.find_element(By.NAME, 'login').send_keys(Keys.ENTER)
48+
49+
WebDriverWait(driver = self.driver, timeout = 10).\
50+
until(lambda d: d.find_element_by_link_text("Event Coder Interface"))
51+
52+
## Head to event page and click add-event
53+
self.driver.get("http://cliff.ssc.wisc.edu/campus_protest_dev/event_creator/319")
54+
addevent = WebDriverWait(driver = self.driver, timeout = 10).\
55+
until(lambda d: d.find_element_by_id("add-event"))
56+
addevent.click()
57+
58+
## wait until article-desc box loads
59+
WebDriverWait(driver = self.driver, timeout = 10).\
60+
until(lambda d: d.find_element_by_id("info_article-desc"))
61+
62+
63+
def tearDown(self):
64+
## Logout
65+
print("TEAR DOWN")
66+
67+
## remove all test codings
68+
cecs = database.db_session.query(CodeEventCreator).filter(CodeEventCreator.coder_id == 2)
69+
70+
## remove all the test events
71+
events = database.db_session.query(Event).filter(Event.id.in_([x.event_id for x in cecs]))
72+
73+
cecs.delete()
74+
events.delete()
75+
database.db_session.commit()
76+
77+
## close the session
78+
database.db_session.close()
79+
80+
## shut down firefox
81+
self.driver.quit()
82+
83+
###
84+
## Tests
85+
###
86+
87+
## Test adding descriptions
88+
def test_add_desc(self):
89+
## find text box and add descriptions
90+
self.driver.find_element_by_id("info_article-desc").send_keys("This is a test of adding text.")
91+
self.driver.find_element_by_id("info_desc").send_keys("Adding text to the event description.")
92+
93+
## change focus to save
94+
self.driver.find_element_by_id("info_start-date").click()
95+
96+
## get the fields from the database
97+
q = database.db_session.query(CodeEventCreator).\
98+
filter(CodeEventCreator.coder_id == 2)
99+
texts = [x.value for x in q]
100+
101+
self.assertIn("This is a test of adding text.", texts)
102+
self.assertIn("Adding text to the event description.", texts)
103+
104+
105+
## test adding of dates and location
106+
def test_add_dates(self):
107+
d = {}
108+
self.driver.find_element_by_id("info_start-date").send_keys("2020-07-20")
109+
self.driver.find_element_by_id("info_end-date").send_keys("2020-07-21")
110+
self.driver.find_element_by_id("info_location").send_keys("Chicago, IL, USA")
111+
112+
## changes focus to start-date
113+
self.driver.find_element_by_id("info_start-date").click()
114+
115+
for a in database.db_session.query(CodeEventCreator).filter(CodeEventCreator.coder_id == 2).all():
116+
d[a.variable] = a.value
117+
118+
self.assertIn("2020-07-20", d["start-date"])
119+
self.assertIn("2020-07-21", d["end-date"])
120+
self.assertIn("Chicago, IL, USA", d["location"])
121+
122+
123+
## Test the Yes/No pane
124+
def test_yesno(self):
125+
yesno = WebDriverWait(driver = self.driver, timeout = 10).\
126+
until(lambda d: d.find_element_by_id("yes-no_button"))
127+
yesno.click()
128+
129+
test_dict = {
130+
'date-est': 'exact',
131+
'duration': 'one',
132+
'non-campus': 'yes',
133+
'off-campus': 'yes'
134+
}
135+
136+
for variable in test_dict.keys():
137+
el = self.driver.find_element_by_id('info_{}'.format(variable))
138+
el.click()
139+
140+
qs = database.db_session.query(CodeEventCreator).filter(CodeEventCreator.coder_id == 2).all()
141+
142+
for q in qs:
143+
self.assertEqual(test_dict[q.variable], q.value)
144+
145+
## TK: This is not quite working yet.
146+
@unittest.skip("Skipping text selects for now.")
147+
def test_text_select(self):
148+
textselect = WebDriverWait(driver = self.driver, timeout = 10).\
149+
until(lambda d: d.find_element_by_id("textselect_button"))
150+
textselect.click()
151+
152+
## Load from YAML
153+
fh = open('../text-selects.yaml', 'r')
154+
test_fields = [x for x in yaml.load(fh, Loader = yaml.BaseLoader).keys()]
155+
fh.close()
156+
157+
## get the grafs
158+
grafs = self.driver.find_element_by_id("bodytext").find_elements(By.TAG_NAME, "p")
159+
160+
## set the average length of text fields
161+
avg_length = 50
162+
163+
for variable in test_fields:
164+
## Select some random body text by picking a random graf
165+
## and getting some random text
166+
graf = random.choice(grafs)
167+
graf.click()
168+
169+
graf_len = len(graf.text)
170+
offset = random.choice(range(avg_length)) if graf_len > avg_length else graf_len
171+
172+
print(variable, offset)
173+
174+
## move over to the n-th character
175+
# for _ in range(start):
176+
# webdriver.ActionChains(self.driver).send_keys(Keys.ARROW_RIGHT).perform()
177+
178+
## press left shift
179+
webdriver.ActionChains(self.driver).key_up(Keys.LEFT_SHIFT).perform()
180+
181+
## move to n+kth character
182+
for _ in range(offset):
183+
webdriver.ActionChains(self.driver).send_keys(Keys.ARROW_RIGHT).perform()
184+
185+
## lift up left shift
186+
webdriver.ActionChains(self.driver).key_up(Keys.LEFT_SHIFT).perform()
187+
188+
## click an add button
189+
self.driver.find_element_by_id("add_{}".format(variable)).click()
190+
191+
## get the selected text but wait until it loads
192+
## TK: this is timing out?? need to find out why this is happening
193+
selected_text = WebDriverWait(driver = self.driver, timeout = 10).\
194+
until(lambda d: d.find_element_by_id("list_{}".format(variable)).\
195+
find_element_by_tag_name("p"))
196+
197+
## clear selected text by clicking on the graf
198+
graf.click()
199+
200+
## Assert that the addition has ended up in the list
201+
self.assertIsNot(selected_text, "")
202+
203+
vals = {}
204+
qs = database.db_session.query(CodeEventCreator).filter(CodeEventCreator.coder_id == 2).all()
205+
206+
## ensure that the number of fields stored is equal
207+
self.assertEqual(len(qs), len(test_fields))
208+
209+
for q in qs:
210+
vals[q.variable] = q.text
211+
212+
print(q.variable, q.value, q.text)
213+
self.assertIsNot(q.text, None)
214+
215+
216+
## test presets
217+
def test_presets(self):
218+
presets = WebDriverWait(driver = self.driver, timeout = 10).\
219+
until(lambda d: d.find_element_by_id("preset_button"))
220+
presets.click()
221+
222+
## Load from YAML
223+
fh = open('../presets.yaml', 'r')
224+
preset_fields = yaml.load(fh, Loader = yaml.BaseLoader)
225+
fh.close()
226+
227+
## stored values to check
228+
d = {}
229+
230+
## select form, issue, racial issue, and target
231+
for variable in (['form', 'issue', 'racial-issue', 'target']):
232+
self.driver.find_element_by_id("l_varevent_{}".format(variable)).click()
233+
d[variable] = []
234+
235+
## select three random items
236+
for value in random.sample(preset_fields[variable], 3):
237+
## wait until we can click
238+
## from https://stackoverflow.com/questions/56085152/selenium-python-error-element-could-not-be-scrolled-into-view
239+
element = WebDriverWait(self.driver, 10).until(\
240+
EC.element_to_be_clickable((By.CSS_SELECTOR, "input[value='{}']".format(value))))
241+
element.location_once_scrolled_into_view
242+
element.click()
243+
244+
## append to stored value dict
245+
d[variable].append(value)
246+
247+
qs = database.db_session.query(CodeEventCreator).filter(CodeEventCreator.coder_id == 2).all()
248+
249+
## ensure that the number of fields stored
250+
## is equal to number of fields * number selected (4 * 3 = 12)
251+
self.assertEqual(len(qs), 12)
252+
253+
## check if the value was selected
254+
for q in qs:
255+
self.assertIn(q.value, d[q.variable])
256+
257+
258+
if __name__ == "__main__":
259+
unittest.main()

‎tests/test_login.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import csv
2+
import unittest
3+
4+
from selenium.webdriver import Firefox, FirefoxOptions
5+
from selenium.webdriver.common.by import By
6+
from selenium.webdriver.common.keys import Keys
7+
8+
from selenium.webdriver.support.ui import WebDriverWait
9+
10+
class LoginTest(unittest.TestCase):
11+
opts = FirefoxOptions()
12+
opts.add_argument("--headless")
13+
driver = Firefox(options=opts)
14+
15+
def setUp(self):
16+
users = {}
17+
with open('credentials.csv', newline='') as csvfile:
18+
reader = csv.reader(csvfile, delimiter=',')
19+
for row in reader:
20+
users[row[0]] = row[1]
21+
22+
print("Navigating to homepage...")
23+
self.driver.get("http://cliff.ssc.wisc.edu/campus_protest_dev/adj")
24+
25+
print("Logging in...")
26+
self.driver.find_element(By.NAME, 'username').send_keys('adj1')
27+
self.driver.find_element(By.NAME, 'password').send_keys(users['adj1'])
28+
self.driver.find_element(By.NAME, 'login').send_keys(Keys.ENTER)
29+
30+
def tearDown(self):
31+
self.driver.quit()
32+
33+
## Tests
34+
def test_login(self):
35+
el = WebDriverWait(driver = self.driver, timeout = 10).\
36+
until(lambda d: d.find_element_by_link_text("Adjudication Interface"))
37+
self.assertEqual(el.text, "Adjudication Interface")
38+
39+
if __name__ == "__main__":
40+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.