Browse Source

I should commit more frequently.

pull/1/head
Dashie der otter 3 years ago
parent
commit
a226ff211a
Signed by: Dashie <dashie@sigpipe.me> GPG Key ID: C2D57B325840B755
100 changed files with 26865 additions and 9 deletions
  1. 5
    0
      .gitignore
  2. 6
    0
      .gitmodules
  3. 15
    9
      README.md
  4. 33
    0
      adif.py
  5. 169
    0
      ahrl.py
  6. 40
    0
      config.py.sample
  7. 0
    0
      controllers/__init__.py
  8. 31
    0
      controllers/api.py
  9. 16
    0
      controllers/main.py
  10. 68
    0
      controllers/notes.py
  11. 381
    0
      controllers/qsos.py
  12. 159
    0
      controllers/tools.py
  13. 42
    0
      controllers/users.py
  14. 19
    0
      crons.py
  15. 4190
    0
      dbseed.py
  16. 142
    0
      forms.py
  17. 46
    0
      libjambon.py
  18. 1
    0
      migrations/README
  19. 45
    0
      migrations/alembic.ini
  20. 87
    0
      migrations/env.py
  21. 22
    0
      migrations/script.py.mako
  22. 328
    0
      migrations/versions/a8a7c1e2e3a_.py
  23. 394
    0
      models.py
  24. 587
    0
      static/css/bootstrap-theme.css
  25. 1
    0
      static/css/bootstrap-theme.css.map
  26. 6
    0
      static/css/bootstrap-theme.min.css
  27. 1
    0
      static/css/bootstrap-theme.min.css.map
  28. 6760
    0
      static/css/bootstrap.css
  29. 1
    0
      static/css/bootstrap.css.map
  30. 6
    0
      static/css/bootstrap.min.css
  31. 1
    0
      static/css/bootstrap.min.css.map
  32. BIN
      static/css/chosen-sprite.png
  33. BIN
      static/css/chosen-sprite@2x.png
  34. 450
    0
      static/css/chosen.css
  35. 3
    0
      static/css/chosen.min.css
  36. 239
    0
      static/css/font-awesome.css
  37. 4
    0
      static/css/font-awesome.min.css
  38. 15
    0
      static/css/ie10-viewport-bug-workaround.css
  39. BIN
      static/css/images/marker-icon-2x.png
  40. BIN
      static/css/images/markers-matte.png
  41. BIN
      static/css/images/markers-matte@2x.png
  42. BIN
      static/css/images/markers-plain.png
  43. BIN
      static/css/images/markers-shadow.png
  44. BIN
      static/css/images/markers-shadow@2x.png
  45. BIN
      static/css/images/markers-soft.png
  46. BIN
      static/css/images/markers-soft@2x.png
  47. BIN
      static/css/images/ui-anim_basic_16x16.gif
  48. BIN
      static/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png
  49. BIN
      static/css/images/ui-bg_diagonals-thick_20_666666_40x40.png
  50. BIN
      static/css/images/ui-bg_flat_0_888888_40x100.png
  51. BIN
      static/css/images/ui-bg_flat_0_aaaaaa_40x100.png
  52. BIN
      static/css/images/ui-bg_flat_10_000000_40x100.png
  53. BIN
      static/css/images/ui-bg_flat_75_ffffff_40x100.png
  54. BIN
      static/css/images/ui-bg_glass_100_f6f6f6_1x400.png
  55. BIN
      static/css/images/ui-bg_glass_100_fdf5ce_1x400.png
  56. BIN
      static/css/images/ui-bg_glass_25_e1f0f5_1x400.png
  57. BIN
      static/css/images/ui-bg_glass_55_444444_1x400.png
  58. BIN
      static/css/images/ui-bg_glass_65_ffffff_1x400.png
  59. BIN
      static/css/images/ui-bg_glass_75_dadada_1x400.png
  60. BIN
      static/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png
  61. BIN
      static/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
  62. BIN
      static/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png
  63. BIN
      static/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
  64. BIN
      static/css/images/ui-bg_inset-soft_95_fef1ec_1x100.png
  65. BIN
      static/css/images/ui-icons_222222_256x240.png
  66. BIN
      static/css/images/ui-icons_228ef1_256x240.png
  67. BIN
      static/css/images/ui-icons_309bbf_256x240.png
  68. BIN
      static/css/images/ui-icons_454545_256x240.png
  69. BIN
      static/css/images/ui-icons_bf3030_256x240.png
  70. BIN
      static/css/images/ui-icons_ef8c08_256x240.png
  71. BIN
      static/css/images/ui-icons_ffd27a_256x240.png
  72. BIN
      static/css/images/ui-icons_ffffff_256x240.png
  73. 1225
    0
      static/css/jquery-ui.css
  74. 7
    0
      static/css/jquery-ui.min.css
  75. 833
    0
      static/css/jquery-ui.structure.css
  76. 5
    0
      static/css/jquery-ui.structure.min.css
  77. 410
    0
      static/css/jquery-ui.theme.css
  78. 5
    0
      static/css/jquery-ui.theme.min.css
  79. 124
    0
      static/css/leaflet.awesome-markers.css
  80. 479
    0
      static/css/leaflet.css
  81. 2
    0
      static/css/morris.css
  82. 62
    0
      static/css/pygments.css
  83. 78
    0
      static/css/style.css
  84. 180
    0
      static/css/timeline.css
  85. 18
    0
      static/datatables/Contributing.md
  86. 53
    0
      static/datatables/Readme.md
  87. 762
    0
      static/datatables/examples/advanced_init/column_render.html
  88. 755
    0
      static/datatables/examples/advanced_init/complex_header.html
  89. 746
    0
      static/datatables/examples/advanced_init/defaults.html
  90. 755
    0
      static/datatables/examples/advanced_init/dom_multiple_elements.html
  91. 756
    0
      static/datatables/examples/advanced_init/dom_toolbar.html
  92. 761
    0
      static/datatables/examples/advanced_init/dt_events.html
  93. 745
    0
      static/datatables/examples/advanced_init/events_live.html
  94. 743
    0
      static/datatables/examples/advanced_init/footer_callback.html
  95. 745
    0
      static/datatables/examples/advanced_init/html5-data-attributes.html
  96. 744
    0
      static/datatables/examples/advanced_init/html5-data-options.html
  97. 73
    0
      static/datatables/examples/advanced_init/index.html
  98. 742
    0
      static/datatables/examples/advanced_init/language_file.html
  99. 744
    0
      static/datatables/examples/advanced_init/length_menu.html
  100. 0
    0
      static/datatables/examples/advanced_init/object_dom_read.html

+ 5
- 0
.gitignore View File

@@ -0,0 +1,5 @@
1
+*.pyc
2
+config.py
3
+TODO.org
4
+.idea
5
+*.db

+ 6
- 0
.gitmodules View File

@@ -0,0 +1,6 @@
1
+[submodule "texttable"]
2
+	path = texttable
3
+	url = https://github.com/bufordtaylor/python-texttable.git
4
+[submodule "dxcc"]
5
+	path = dxcc
6
+	url = https://github.com/rhaamo/dxcc.git

+ 15
- 9
README.md View File

@@ -1,34 +1,40 @@
1 1
 Another Ham Radio Log
2 2
 =====================
3 3
 
4
-
5
-
6 4
 # Installation
7 5
     Install a BDD (sqlite, mysql, postgresql)
8 6
     Makes sure that encoding is/will be in UNICODE/UTF-8
9
-    git clone http://dev.sigpipe.me/dashie/ahrl
7
+    git clone http://dev.sigpipe.me/DashieHam/ahrl
10 8
     cd ahrl
11 9
     git submodule init
12 10
     git submodule update
13
-    pip install --requirement requirements.txt
11
+    pip install --requirement requirements.txt  # if present
14 12
     cp config.py.sample config.py
15 13
     $EDITOR config.py
16 14
     python ahrl.py db upgrade
17 15
     python ahrl.py db_seed
18
-    python ahrl.py mkdirs
19 16
     python ahrl.py runserver # or whatever gunicorn whatever stuff
20 17
 
21 18
 # Gunicorn
22 19
     gunicorn -w 2 -b 127.0.0.1:8000 --error-logfile=errors.log --access-logfile=access.log --chdir=$PWD ahrl:app
23 20
 
24 21
 # Crontabs
25
-    TBD
22
+  List of cron target availables.
23
+  Makes sure to run them under the user which runs ahrl and virtualenv if you use it.
24
+  Commands:
25
+  - python ahrl.py cron_update_qsos_countries  # Update all QSOs if missing a Country/DXCC entry by using ClubLog
26 26
 
27 27
 # Licensing
28 28
  - MIT License
29 29
 
30
+# Fork from
31
+ - This is majoritary a fork of https://github.com/magicbug/Cloudlog by 2E0SQL
32
+ - Thanks to him for all his work on CloudLog which inspired me (and from which I reused some things)
33
+
30 34
 # TODO
31
- - Unicorns
32
- - More unicorns
33
- - Even more unicorns !
35
+ - eQSL integration
36
+ - HAMQTH integration
37
+ - QSO edit
38
+ - More statistics
39
+ - Radio/CAT edit, add you own rig and link them when QSOing
34 40
 

+ 33
- 0
adif.py View File

@@ -0,0 +1,33 @@
1
+import re
2
+import datetime
3
+
4
+# Comes from https://web.bxhome.org/content/adifpy
5
+
6
+ADIF_REC_RE = re.compile(r'<(.*?):(\d+).*?>([^<\t\f\v]+)')
7
+
8
+
9
+def parse(s):
10
+    raw = re.split('<eor>|<eoh>(?i)', s)
11
+    logbook = []
12
+    for record in raw[1:-1]:
13
+        qso = {}
14
+        tags = ADIF_REC_RE.findall(record)
15
+        for tag in tags:
16
+                qso[tag[0].lower()] = tag[2][:int(tag[1])]
17
+        logbook.append(qso)
18
+    return logbook
19
+
20
+
21
+def save(fn, data):
22
+    fh = open(fn, 'w')
23
+    fh.write('ADIF.PY by OK4BX\nhttp://web.bxhome.org\n<EOH>\n')
24
+    for qso in data:
25
+        for key in sorted(qso):
26
+            value = qso[key]
27
+            fh.write('<%s:%i>%s  ' % (key.upper(), len(value), value))
28
+        fh.write('<EOR>\n')
29
+    fh.close()
30
+
31
+
32
+def conv_datetime(adi_date, adi_time):
33
+    return datetime.datetime.strptime(adi_date+adi_time.ljust(6, "0"), "%Y%m%d%H%M%S")

+ 169
- 0
ahrl.py View File

@@ -0,0 +1,169 @@
1
+from pprint import pprint as pp
2
+
3
+from flask import Flask, render_template, g, send_from_directory, jsonify
4
+from flask.ext.security import Security, current_user
5
+from flask.ext.script import Manager
6
+from flask.ext.migrate import Migrate, MigrateCommand
7
+from flask.ext.mail import Mail
8
+from flask_bootstrap import Bootstrap
9
+from flask_debugtoolbar import DebugToolbarExtension
10
+
11
+from models import db, user_datastore
12
+
13
+from controllers.main import bp_main
14
+from controllers.users import bp_users
15
+from controllers.notes import bp_notes
16
+from controllers.qsos import bp_qsos
17
+from controllers.tools import bp_tools
18
+from crons import update_qsos_without_countries
19
+
20
+import texttable
21
+from dbseed import make_db_seed
22
+from forms import ExtendedRegisterForm
23
+
24
+import os
25
+import subprocess
26
+
27
+from utils import dt_utc_to_user_tz, InvalidUsage, show_date_no_offset
28
+
29
+import logging
30
+from logging.handlers import RotatingFileHandler
31
+
32
+__VERSION__ = "0.0.1"
33
+
34
+# App Configuration
35
+app = Flask(__name__)
36
+Bootstrap(app)
37
+
38
+app.jinja_env.add_extension('jinja2.ext.with_')
39
+app.jinja_env.filters['localize'] = dt_utc_to_user_tz
40
+app.jinja_env.filters['show_date_no_offset'] = show_date_no_offset
41
+
42
+app.config.from_pyfile("config.py")
43
+
44
+# Logging
45
+if not app.debug:
46
+    formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
47
+    file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), 'a', 1000000, 1)
48
+    file_handler.setLevel(logging.DEBUG)
49
+    file_handler.setFormatter(formatter)
50
+    app.logger.addHandler(file_handler)
51
+
52
+toolbar = DebugToolbarExtension(app)
53
+mail = Mail(app)
54
+
55
+db.init_app(app)
56
+
57
+migrate = Migrate(app, db)
58
+
59
+manager = Manager(app)
60
+
61
+# Setup Flask-Security
62
+security = Security(app, user_datastore,
63
+                    register_form=ExtendedRegisterForm)
64
+
65
+git_version = ""
66
+gitpath = os.path.join(os.getcwd(), ".git")
67
+if os.path.isdir(gitpath):
68
+    git_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
69
+    if git_version:
70
+        git_version = git_version.strip()
71
+
72
+
73
+@app.before_request
74
+def before_request():
75
+    g.cfg = {
76
+        'AHRL_VERSION': "{0} ({1})".format(__VERSION__, git_version),
77
+    }
78
+    g.current_user = current_user
79
+
80
+
81
+@app.errorhandler(InvalidUsage)
82
+def handle_invalid_usage(error):
83
+    response = jsonify(error.to_dict())
84
+    response.status_code = error.status_code
85
+    return response
86
+
87
+app.register_blueprint(bp_main)
88
+app.register_blueprint(bp_users)
89
+app.register_blueprint(bp_notes)
90
+app.register_blueprint(bp_qsos)
91
+app.register_blueprint(bp_tools)
92
+
93
+
94
+# Used in development
95
+@app.route('/uploads/<path:stuff>', methods=['GET'])
96
+def get_uploads_stuff(stuff):
97
+    print("Get {0} from {1}".format(stuff, app.config['UPLOADS_DEFAULT_DEST']))
98
+    return send_from_directory(['UPLOADS_DEFAULT_DEST'], stuff, as_attachment=False)
99
+
100
+
101
+@app.errorhandler(404)
102
+def page_not_found(e):
103
+    pcfg = {"title": "Whoops, something failed.",
104
+            "error": 404, "message": "Page not found", "e": e}
105
+    return render_template('error_page.jinja2', pcfg=pcfg), 404
106
+
107
+
108
+@app.errorhandler(403)
109
+def err_forbidden(e):
110
+    pcfg = {"title": "Whoops, something failed.",
111
+            "error": 403, "message": "Access forbidden", "e": e}
112
+    return render_template('error_page.jinja2', pcfg=pcfg), 403
113
+
114
+
115
+@app.errorhandler(410)
116
+def err_gone(e):
117
+    pcfg = {"title": "Whoops, something failed.",
118
+            "error": 410, "message": "Gone", "e": e}
119
+    return render_template('error_page.jinja2', pcfg=pcfg), 410
120
+
121
+
122
+if not app.debug:
123
+    @app.errorhandler(500)
124
+    def err_failed(e):
125
+        pcfg = {"title": "Whoops, something failed.", "error": 500, "message": "Something is broken", "e": e}
126
+        return render_template('error_page.jinja2', pcfg=pcfg), 500
127
+
128
+
129
+# Other commands
130
+@manager.command
131
+def dump_routes():
132
+    """Dump all routes of defined app"""
133
+    table = texttable.Texttable()
134
+    table.set_deco(texttable.Texttable().HEADER)
135
+    table.set_cols_dtype(['t', 't', 't'])
136
+    table.set_cols_align(["l", "l", "l"])
137
+    table.set_cols_width([60, 30, 90])
138
+
139
+    table.add_rows([["Prefix", "Verb", "URI Pattern"]])
140
+
141
+    for rule in sorted(app.url_map.iter_rules(), key=lambda x: x.match_compare_key()):
142
+        methods = ','.join(rule.methods)
143
+        table.add_row([rule.endpoint, methods, rule])
144
+
145
+    print(table.draw())
146
+
147
+
148
+@manager.command
149
+def config():
150
+    """Dump config"""
151
+    pp(app.config)
152
+
153
+
154
+@manager.command
155
+def db_seed():
156
+    """Seed database with default content"""
157
+    make_db_seed(db)
158
+
159
+
160
+@manager.command
161
+def cron_update_qsos_countries():
162
+    """Update QSOs with empty country"""
163
+    update_qsos_without_countries(db)
164
+
165
+
166
+manager.add_command('db', MigrateCommand)
167
+
168
+if __name__ == '__main__':
169
+    manager.run()

+ 40
- 0
config.py.sample View File

@@ -0,0 +1,40 @@
1
+DEBUG = True
2
+
3
+SECRET_KEY = 'bleh23r29-r2-9jwehoiqrpj'
4
+#SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://dashie@localhost/ahrl'
5
+# SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://dashie@localhost/ahrl'
6
+SQLALCHEMY_DATABASE_URI = 'sqlite:///ahrl.db'
7
+SQLALCHEMY_ECHO=False
8
+
9
+SECURITY_CONFIRMABLE = False
10
+SECURITY_REGISTERABLE = False  # deactivate registration
11
+SECURITY_RECOVERABLE = True
12
+SECURITY_TRACKABLE = False
13
+SECURITY_CHANGEABLE = True
14
+SECURITY_PASSWORD_HASH = 'bcrypt'
15
+SECURITY_PASSWORD_SALT = 'omgponies'
16
+# SECURITY_URL_PREFIX = '/sec'
17
+
18
+SECURITY_SEND_REGISTER_EMAIL = False
19
+SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False
20
+SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False
21
+
22
+BOOTSTRAP_USE_MINIFIED = True
23
+BOOTSTRAP_SERVE_LOCAL = True
24
+BOOTSTRAP_CDN_FORCE_SSL = True
25
+BOOTSTRAP_QUERYSTRING_REVVING = True
26
+
27
+DEBUG_TB_PROFILER_ENABLED = True
28
+DEBUG_TB_INTERCEPT_REDIRECTS = False
29
+
30
+# Paginations
31
+
32
+CLUBLOG_API_KEY=""
33
+
34
+LOOKUP_METHOD="hamqth"
35
+
36
+QRZ_USERNAME=""
37
+QRZ_PASSWORD=""
38
+
39
+HAMQTH_USERNAME=""
40
+HAMQTH_PASSWORD=""

+ 0
- 0
controllers/__init__.py View File


+ 31
- 0
controllers/api.py View File

@@ -0,0 +1,31 @@
1
+from flask import Blueprint, redirect, url_for, abort
2
+from flask.ext.security import login_required, current_user
3
+from models import db, Apitoken
4
+from utils import generate_uniques_apitoken
5
+
6
+bp_api = Blueprint('bp_api', __name__)
7
+
8
+
9
+@bp_api.route('/apitoken/new')
10
+@login_required
11
+def apitoken_new():
12
+    apitoken = generate_uniques_apitoken()
13
+    if not apitoken:
14
+        return abort(500)
15
+
16
+    a = Apitoken()
17
+    a.user_id = current_user.id
18
+    a.token = apitoken["token"]
19
+    a.secret = apitoken["secret"]
20
+    db.session.add(a)
21
+    db.session.commit()
22
+    return redirect(url_for('bp_users.user_profile'))
23
+
24
+
25
+@bp_api.route('/apitoken/<string:apit>/del')
26
+@login_required
27
+def apitoken_del(apit):
28
+    apitoken = Apitoken.query.get_or_404(apit)
29
+    db.session.delete(apitoken)
30
+    db.session.commit()
31
+    return redirect(url_for('bp_users.user_profile'))

+ 16
- 0
controllers/main.py View File

@@ -0,0 +1,16 @@
1
+from flask import Blueprint, render_template
2
+
3
+bp_main = Blueprint('bp_main', __name__)
4
+
5
+
6
+# Show public logbooks
7
+@bp_main.route('/')
8
+def home():
9
+    pcfg = {"title": "AHRL - Another Ham Radio Log"}
10
+    return render_template('home.jinja2', pcfg=pcfg)
11
+
12
+
13
+@bp_main.route('/about')
14
+def about():
15
+    pcfg = {"title": "About AHRL - Another Ham Radio Log"}
16
+    return render_template('about.jinja2', pcfg=pcfg)

+ 68
- 0
controllers/notes.py View File

@@ -0,0 +1,68 @@
1
+from flask import Blueprint, render_template, request, redirect, url_for
2
+from flask.ext.security import login_required, current_user
3
+from models import db, Note
4
+from forms import NoteForm
5
+from utils import check_default_profile
6
+
7
+bp_notes = Blueprint('bp_notes', __name__)
8
+
9
+
10
+@bp_notes.route('/notes', methods=['GET'])
11
+@login_required
12
+@check_default_profile
13
+def notes():
14
+    pcfg = {"title": "My notes"}
15
+    _notes = Note.query.all()
16
+    return render_template('notes/view.jinja2', pcfg=pcfg, notes=_notes)
17
+
18
+
19
+@bp_notes.route('/notes/<int:note_id>/edit', methods=['GET', 'POST'])
20
+@login_required
21
+@check_default_profile
22
+def edit(note_id):
23
+    pcfg = {"title": "Edit my notes"}
24
+    a = Note.query.get_or_404(note_id)
25
+
26
+    form = NoteForm(request.form, a)
27
+
28
+    if form.validate_on_submit():
29
+        a.title = form.title.data
30
+        a.cat = form.cat.data
31
+        a.note = form.note.data
32
+
33
+        db.session.commit()
34
+        return redirect(url_for('bp_notes.notes'))
35
+
36
+    return render_template('notes/edit.jinja2', pcfg=pcfg, form=form, note=a, note_id=note_id)
37
+
38
+
39
+@bp_notes.route('/notes/new', methods=['GET', 'POST'])
40
+@login_required
41
+@check_default_profile
42
+def new():
43
+    pcfg = {"title": "New note"}
44
+
45
+    form = NoteForm()
46
+
47
+    if form.validate_on_submit():
48
+        a = Note()
49
+        a.title = form.title.data
50
+        a.cat = form.cat.data
51
+        a.note = form.note.data
52
+        a.user_id = current_user.id
53
+
54
+        db.session.add(a)
55
+        db.session.commit()
56
+        return redirect(url_for('bp_notes.notes'))
57
+
58
+    return render_template('notes/new.jinja2', pcfg=pcfg, form=form)
59
+
60
+
61
+@bp_notes.route('/notes/<int:note_id>/delete', methods=['GET', 'DELETE', 'PUT'])
62
+@login_required
63
+@check_default_profile
64
+def delete(note_id):
65
+    note = Note.query.get_or_404(note_id)
66
+    db.session.delete(note)
67
+    db.session.commit()
68
+    return redirect(url_for('bp_notes.notes'))

+ 381
- 0
controllers/qsos.py View File

@@ -0,0 +1,381 @@
1
+from flask import Blueprint, render_template, request, redirect, url_for, Response, json, abort
2
+from flask.ext.security import login_required, current_user
3
+from models import db, User, Log
4
+from forms import QsoForm
5
+import pytz
6
+import datetime
7
+from libjambon import band_to_frequency, geo_bearing_star
8
+from utils import InvalidUsage, dt_utc_to_user_tz, check_default_profile, get_dxcc_from_clublog
9
+from geohelper import distance, bearing
10
+from libqth import is_valid_qth, qth_to_coords
11
+from calendar import monthrange
12
+
13
+bp_qsos = Blueprint('bp_qsos', __name__)
14
+
15
+
16
+@bp_qsos.route('/logbook/<string:username>', methods=['GET'])
17
+@check_default_profile
18
+def logbook(username):
19
+    user = User.query.filter(User.name == username).first()
20
+    if not user:
21
+        return abort(404)
22
+    pcfg = {"title": "{0}'s ({1}) logbook".format(user.name, user.callsign)}
23
+    qsos = Log.query.filter(User.id == user.id).all()
24
+    uqth = user.qth_to_coords()
25
+
26
+    d = datetime.datetime.utcnow()
27
+    mr_m = monthrange(d.year, d.month)
28
+    mr_y = monthrange(d.year, 12)
29
+
30
+    d_month_start = datetime.datetime(d.year, d.month, 0o1, 00, 00, 00, tzinfo=pytz.timezone('UTC'))
31
+    d_month_end = datetime.datetime(d.year, d.month, mr_m[1], 23, 59, 59, tzinfo=pytz.timezone('UTC'))
32
+    d_year_start = datetime.datetime(d.year, 0o1, 0o1, 00, 00, 00, tzinfo=pytz.timezone('UTC'))
33
+    d_year_end = datetime.datetime(d.year, 12, mr_y[1], 23, 59, 59, tzinfo=pytz.timezone('UTC'))
34
+    cntry_worked = db.session.query(Log.country).filter(Log.user_id == user.id).distinct(Log.country).count()
35
+
36
+    stats = {
37
+        'qsos': {
38
+            'total': Log.query.filter(Log.user_id == user.id).count(),
39
+            'month': Log.query.filter(Log.user_id == user.id,
40
+                                      Log.time_on.between(d_month_start, d_month_end)).count(),
41
+            'year': Log.query.filter(Log.user_id == user.id,
42
+                                     Log.time_on.between(d_year_start, d_year_end)).count()
43
+        },
44
+        'countries': {
45
+            'worked': cntry_worked,
46
+            'needed': 340 - cntry_worked
47
+        },
48
+        'qsl': {
49
+            'sent': Log.query.filter(Log.user_id == user.id, Log.qsl_sent == 'Y').count(),
50
+            'received': Log.query.filter(Log.user_id == user.id, Log.qsl_rcvd == 'Y').count(),
51
+            'requested': Log.query.filter(Log.user_id == user.id, Log.qsl_sent == 'R').count()
52
+        }
53
+    }
54
+
55
+    return render_template('qsos/logbook.jinja2', pcfg=pcfg, logbook=qsos, user=user, uqth=uqth, stats=stats)
56
+
57
+
58
+@bp_qsos.route('/qsos/new/<string:method>', methods=['GET', 'POST'])
59
+@login_required
60
+@check_default_profile
61
+def new(method):
62
+    if method not in ['live', 'manual']:
63
+        return redirect(url_for('bp_qsos.logbook', user_id=current_user.id))
64
+
65
+    pcfg = {"title": "New QSO"}
66
+
67
+    form = QsoForm()
68
+
69
+    if form.validate_on_submit():
70
+        a = Log()
71
+        # We get from user input a date with timezone offset (we asume that)
72
+        # We store in database in UTC offset, so first convert to DT then makes aware
73
+        # of the user timezone, and then convert to UTC
74
+        date = form.date.data.strftime('%d-%m-%Y')
75
+        time = form.time.data.strftime('%H:%M:%S')
76
+        date_wo_tz = datetime.datetime.strptime("{0} {1}".format(date, time),
77
+                                                "%d-%m-%Y %H:%M:%S")
78
+        date_w_tz = pytz.timezone(current_user.timezone).localize(date_wo_tz)
79
+
80
+        a.time_on = date_w_tz.astimezone(pytz.timezone('UTC'))
81
+        a.time_off = date_w_tz.astimezone(pytz.timezone('UTC'))
82
+        a.call = form.callsign.data
83
+        a.freq = form.frequency.data
84
+        a.rst_rcvd = form.rst_r.data
85
+        a.rst_sent = form.rst_s.data
86
+        a.name = form.name.data
87
+        a.comment = form.comment.data
88
+        a.sat_name = form.sat_name.data
89
+        a.sat_mode = form.sat_mode.data
90
+        a.gridsquare = form.locator.data
91
+        a.country = form.country.data
92
+        a.qsl_sent = form.qsl_sent.raw_data[0]
93
+        a.qsl_sent_via = form.qsl_method.raw_data[0]
94
+        a.qsl_via = form.qsl_via.data
95
+        a.operator = current_user.callsign
96
+        a.owner_callsign = current_user.callsign
97
+        a.station_callsign = current_user.callsign
98
+        a.qth = form.location.data
99
+        a.prop_mode = form.propagation.raw_data[0]
100
+        a.iota = form.iota.data
101
+        a.my_gridsquare = current_user.locator
102
+        a.dxcc = form.dxcc_id.data
103
+        a.cqz = form.cqz.data
104
+
105
+        if current_user.swl:
106
+            a.swl = 'Y'
107
+        else:
108
+            a.swl = 'N'
109
+
110
+        a.distance = 0  # ??
111
+        a.freq_rx = 0  # ??
112
+
113
+        a.user_id = current_user.id
114
+        a.my_rig = form.radio.raw_data[0]  # TODO relation in model
115
+        a.band_id = form.band.raw_data[0]
116
+        a.mode_id = form.band.raw_data[0]
117
+
118
+        db.session.add(a)
119
+        db.session.commit()
120
+        return redirect(url_for('bp_qsos.new', method=method))
121
+
122
+    qsos = Log.query.filter(User.id == current_user.id).limit(16).all()
123
+
124
+    return render_template('qsos/new.jinja2', pcfg=pcfg, form=form, logbook=qsos, method=method)
125
+
126
+
127
+@bp_qsos.route('/qsos/<int:qso_id>/delete', methods=['GET', 'DELETE', 'PUT'])
128
+@login_required
129
+@check_default_profile
130
+def delete(qso_id):
131
+    qso = Log.query.get_or_404(qso_id)
132
+    db.session.delete(qso)
133
+    db.session.commit()
134
+    return redirect(url_for('bp_qsos.logbook', username=qso.user.name))
135
+
136
+
137
+@bp_qsos.route('/qsos/lib/jambon/band_to_freq', methods=['GET'])
138
+@login_required
139
+def lib_jambon_band_to_freq():
140
+    band = request.args.get('band')
141
+    mode = request.args.get('mode')
142
+
143
+    if not band or not mode:
144
+        raise InvalidUsage('Missing band or mode', status_code=400)
145
+
146
+    response = {'status': 'ok', 'band': band, 'mode': mode, 'frequency': band_to_frequency(int(band), int(mode))}
147
+    return Response(json.dumps(response), mimetype='application/json')
148
+
149
+
150
+@bp_qsos.route('/qsos/lib/geo/bearing', methods=['GET'])
151
+@login_required
152
+def lib_geo_bearing():
153
+    locator_qso = request.args.get('locator_qso')
154
+    locator_user = request.args.get('locator_user')
155
+
156
+    if not locator_qso or not locator_user:
157
+        raise InvalidUsage('Missing locator_qso or locator_user', status_code=400)
158
+
159
+    if not is_valid_qth(locator_user, 6) or not is_valid_qth(locator_qso, 6):
160
+        raise InvalidUsage('One of the supplied QTH is not valid', status_code=400)
161
+
162
+    _f = qth_to_coords(locator_user, 6)  # precision, latitude, longitude
163
+    _t = qth_to_coords(locator_qso, 6)  # precision, latitude, longitude
164
+
165
+    response = {'status': 'ok',
166
+                'locator_qso': locator_qso,
167
+                'locator_user': locator_user,
168
+                'distance': distance.haversine_km(_f['latitude'],
169
+                                                  _f['longitude'],
170
+                                                  _t['latitude'],
171
+                                                  _t['longitude']),
172
+                'unit': 'km',
173
+                'bearing': bearing.initial_compass_bearing(_f['latitude'],
174
+                                                           _f['longitude'],
175
+                                                           _t['latitude'],
176
+                                                           _t['longitude'])}
177
+    response['bearing_star'] = geo_bearing_star(response['bearing'])
178
+
179
+    return Response(json.dumps(response), mimetype='application/json')
180
+
181
+
182
+@bp_qsos.route('/qsos/lib/clublog/dxcc', methods=['GET'])
183
+@login_required
184
+def lib_clublog_dxcc():
185
+    callsign = request.args.get('callsign')
186
+
187
+    if not callsign:
188
+        raise InvalidUsage('Missing callsign', status_code=400)
189
+
190
+    dxcc = get_dxcc_from_clublog(callsign)
191
+    if not dxcc:
192
+        raise InvalidUsage('Error while getting infos from clublog', status_code=500)
193
+
194
+    response = {'status': 'ok'}
195
+    response.update(dxcc)
196
+
197
+    return Response(json.dumps(response), mimetype='application/json')
198
+
199
+
200
+@bp_qsos.route('/logbook/<string:username>/geojson', methods=['GET'])
201
+@login_required
202
+def logbook_geojson(username):
203
+    if not username:
204
+        raise InvalidUsage('Missing username', status_code=400)
205
+
206
+    user = User.query.filter(User.name == username).first()
207
+    if not user:
208
+        raise InvalidUsage('User not found', status_code=404)
209
+
210
+    logs = Log.query.filter(User.id == user.id).all()
211
+
212
+    if not is_valid_qth(user.locator, 6):
213
+        raise InvalidUsage('QTH is not valid', status_code=400)
214
+    _u = qth_to_coords(user.locator, 6)  # precision, latitude, longitude
215
+
216
+    j = [{
217
+        "type": "Feature",
218
+        "properties": {
219
+            "name": user.cutename(),
220
+            "callsign": user.callsign,
221
+            "own": True,
222
+            "icon": "home"
223
+        },
224
+        "geometry": {
225
+            "type": "Point",
226
+            "coordinates": [_u['longitude'], _u['latitude']]
227
+        }
228
+    }]
229
+
230
+    for log in logs:
231
+        if log.gridsquare:
232
+            if not is_valid_qth(log.gridsquare, 6):
233
+                raise InvalidUsage('QTH is not valid', status_code=400)
234
+            _f = qth_to_coords(log.gridsquare, 6)  # precision, latitude, longitude
235
+        else:
236
+            _f = log.country_grid_coords()
237
+            if not _f:
238
+                continue  # No grid at all ? Skit ip
239
+
240
+        f = {
241
+            "type": "Feature",
242
+            "properties": {
243
+                "name": log.cutename(),
244
+                "callsign": log.call,
245
+                "date": dt_utc_to_user_tz(log.time_on, user=user),
246
+                "band": log.band.name,
247
+                "mode": log.mode.mode,
248
+                "icon": "qso"
249
+            },
250
+            "geometry": {
251
+                "type": "Point",
252
+                "coordinates": [_f['longitude'], _f['latitude']]
253
+            }
254
+        }
255
+        j.append(f)
256
+
257
+    return Response(json.dumps(j), mimetype='application/json')
258
+
259
+
260
+@bp_qsos.route('/logbook/qso/<int:qso_id>/geojson', methods=['GET'])
261
+@login_required
262
+def logbook_qso_geojson(qso_id):
263
+    if not qso_id:
264
+        raise InvalidUsage('Missing qso_id', status_code=400)
265
+
266
+    qso = Log.query.filter(Log.id == qso_id).first()
267
+    if not qso:
268
+        raise InvalidUsage('Qso not found', status_code=404)
269
+
270
+    if qso.gridsquare:
271
+        if not is_valid_qth(qso.gridsquare, 6):
272
+            raise InvalidUsage('QTH is not valid', status_code=400)
273
+        _f = qth_to_coords(qso.gridsquare, 6)  # precision, latitude, longitude
274
+    else:
275
+        _f = qso.country_grid_coords()
276
+        if not _f:
277
+            raise InvalidUsage('No valid QTH', status_code=404)
278
+
279
+    f = [{
280
+        "type": "Feature",
281
+        "properties": {
282
+            "name": qso.cutename(),
283
+            "callsign": qso.call,
284
+            "date": dt_utc_to_user_tz(qso.time_on, user=qso.user),
285
+            "band": qso.band.name,
286
+            "mode": qso.mode.mode,
287
+            "icon": 'home'
288
+        },
289
+        "geometry": {
290
+            "type": "Point",
291
+            "coordinates": [_f['longitude'], _f['latitude']]
292
+        }
293
+    }]
294
+
295
+    if not is_valid_qth(qso.user.locator, 6):
296
+        raise InvalidUsage('QTH is not valid', status_code=400)
297
+    _u = qth_to_coords(qso.user.locator, 6)  # precision, latitude, longitude
298
+    g = {
299
+        "type": "Feature",
300
+        "properties": {
301
+            "name": qso.user.cutename(),
302
+            "callsign": qso.user.callsign,
303
+            "own": True,
304
+            "icon": 'qso'
305
+        },
306
+        "geometry": {
307
+            "type": "Point",
308
+            "coordinates": [_u['longitude'], _u['latitude']]
309
+        }
310
+    }
311
+    f.append(g)
312
+
313
+    f.append({
314
+        "type": "LineString",
315
+        "coordinates": [[_f['longitude'], _f['latitude']], [_u['longitude'], _u['latitude']]]
316
+    })
317
+
318
+    return Response(json.dumps(f), mimetype='application/json')
319
+
320
+
321
+@bp_qsos.route('/logbook/qso/<int:qso_id>/modal', methods=['GET'])
322
+def single_qso_modal(qso_id):
323
+    qso = Log.query.get_or_404(qso_id)
324
+    if not qso.gridsquare:
325
+        qso_gs = qso.country_grid()
326
+    else:
327
+        qso_gs = qso.gridsquare
328
+
329
+    if not qso_gs or not qso.user.locator:
330
+        raise InvalidUsage('Missing qso.gridsquare or qso.user.locator', status_code=400)
331
+
332
+    if not is_valid_qth(qso.user.locator, 6) or not is_valid_qth(qso_gs, 6):
333
+        raise InvalidUsage('One of the supplied QTH is not valid', status_code=400)
334
+
335
+    _f = qth_to_coords(qso.user.locator, 6)  # precision, latitude, longitude
336
+    _t = qth_to_coords(qso_gs, 6)  # precision, latitude, longitude
337
+
338
+    qso_distance = distance.haversine_km(_f['latitude'],
339
+                                         _f['longitude'],
340
+                                         _t['latitude'],
341
+                                         _t['longitude'])
342
+
343
+    qso_bearing = bearing.initial_compass_bearing(_f['latitude'],
344
+                                                  _f['longitude'],
345
+                                                  _t['latitude'],
346
+                                                  _t['longitude'])
347
+    qso_bearing_star = geo_bearing_star(qso_bearing)
348
+
349
+    return render_template('qsos/_single_qso_modal.jinja2', qso=qso, qso_distance=qso_distance, qso_bearing=qso_bearing,
350
+                           qso_bearing_star=qso_bearing_star, qso_distance_unit='Km')
351
+
352
+
353
+@bp_qsos.route('/logbook/stats/<string:username>', methods=['GET'])
354
+@check_default_profile
355
+def logbook_stats(username):
356
+    user = User.query.filter(User.name == username).first()
357
+    if not user:
358
+        return abort(404)
359
+    pcfg = {'title': 'Stats'}
360
+
361
+    stats_months = []
362
+
363
+    dt = datetime.datetime.utcnow()
364
+
365
+    for y in [dt.year, dt.year - 1]:
366
+        stats_y = []
367
+        for i in range(1, 13):
368
+            mr = monthrange(y, i)
369
+            d_month_start = datetime.datetime(y, i, 0o1, 00, 00, 00, tzinfo=pytz.timezone('UTC'))
370
+            d_month_end = datetime.datetime(y, i, mr[1], 23, 59, 59, tzinfo=pytz.timezone('UTC'))
371
+            stats_y.append({
372
+                'count': Log.query.filter(Log.user_id == user.id,
373
+                                          Log.time_on.between(d_month_start, d_month_end)).count(),
374
+                'month': i,
375
+                'year': y,
376
+                'label': 'QSOs',
377
+                'date': "{0}-{1}".format(y, i)
378
+            })
379
+        stats_months.append(stats_y)
380
+
381
+    return render_template('qsos/stats.jinja2', pcfg=pcfg, stats_months=json.dumps(stats_months))

+ 159
- 0
controllers/tools.py View File

@@ -0,0 +1,159 @@
1
+from flask import Blueprint, render_template, redirect, url_for, stream_with_context, Response, flash
2
+from flask.ext.security import login_required, current_user
3
+from models import db, Log, Mode, Band
4
+from utils import check_default_profile, ADIF_FIELDS
5
+from adif import parse as adif_parser
6
+from forms import AdifParse
7
+from werkzeug.utils import secure_filename
8
+import datetime
9
+import pytz
10
+
11
+bp_tools = Blueprint('bp_tools', __name__)
12
+
13
+
14
+@bp_tools.route('/tools/adif/import', methods=['GET'])
15
+@login_required
16
+@check_default_profile
17
+def adif_import():
18
+    pcfg = {"title": "Import ADIF"}
19
+    form = AdifParse()
20
+    return render_template('tools/adif_import.jinja2', pcfg=pcfg, form=form)
21
+
22
+
23
+@bp_tools.route('/tools/adif/import', methods=['POST'])
24
+@login_required
25
+@check_default_profile
26
+def adif_import_file():
27
+    pcfg = {"title": "Import ADIF"}
28
+    form = AdifParse()
29
+    if form.validate_on_submit():
30
+        filename = secure_filename(form.adif_file.data.filename)
31
+        files = form.adif_file.raw_data[0].stream.read()
32
+        form.adif_file.raw_data[0].close()
33
+
34
+        parsed_adif = adif_parser(files)
35
+        count = 0
36
+        duplicates = 0
37
+
38
+        for log in parsed_adif:
39
+            # First check if duplicate
40
+            _date = "{0} {1}".format(log['qso_date'], log['time_on'])
41
+            _date_wo_tz = datetime.datetime.strptime(_date, "%Y%m%d %H%M%S")
42
+            dl = Log.query.filter(Log.user_id == current_user.id,
43
+                                  Log.call == log['call'],
44
+                                  Log.time_on == _date_wo_tz)
45
+            if dl:
46
+                duplicates += 1
47
+                continue  # duplicate found, skip record
48
+
49
+            l = Log()
50
+            for key in ADIF_FIELDS:
51
+                if key not in log:
52
+                    continue
53
+
54
+                setattr(l, key, log[key])
55
+            # other fields to manage specifically
56
+            if 'class' in log:
57
+                l.klass = log['class']
58
+            if 'band' in log:
59
+                band = Band.query.filter(Band.name == log['band'],
60
+                                         Band.start.is_(None),
61
+                                         Band.modes.is_(None)).first()
62
+                if not band:
63
+                    band = Mode.query.filter(Band.name == 'SSB',
64
+                                             Band.start.is_(None),
65
+                                             Band.modes.is_(None)).first()
66
+                    l.comment += "\r\nBand automatically set to 40m because not found in ADIF"
67
+                l.band_id = band.id
68
+            if 'freq' in log:
69
+                l.freq = int(float(log['freq']) * 1000000)  # ADIF stores in MHz, we store in Hertz
70
+            if 'freq_rx' in log:
71
+                l.freq_rx = int(float(log['freq_rx']) * 1000000)  # Same as freq
72
+            if 'mode' in log:
73
+                mode = Mode.query.filter(Mode.mode == log['mode']).first()
74
+                if not mode:
75
+                    mode = Mode.query.filter(Mode.mode == 'SSB').first()
76
+                    l.comment += "\r\nMode automatically set to SSB because not found in ADIF"
77
+                l.mode_id = mode.id
78
+            # Reminder : ADIF is in UTC, we store in UTC, no TZ conversion necessary
79
+            if 'qso_date' in log and 'time_on':
80
+                date = "{0} {1}".format(log['qso_date'], log['time_on'])
81
+                date_wo_tz = datetime.datetime.strptime(date, "%Y%m%d %H%M%S")
82
+                date_w_tz = pytz.timezone(current_user.timezone).localize(date_wo_tz)
83
+                l.time_on = date_w_tz.astimezone(pytz.timezone('UTC'))
84
+                l.time_off = date_w_tz.astimezone(pytz.timezone('UTC'))
85
+            else:
86
+                date_w_tz = datetime.datetime.utcnow()
87
+                l.time_on = date_w_tz.astimezone(pytz.timezone('UTC'))
88
+                l.time_off = date_w_tz.astimezone(pytz.timezone('UTC'))
89
+                l.comment += "\r\nDate set to the import date because not found in ADIF"
90
+            l.user = current_user  # oops dont miss it
91
+
92
+            db.session.add(l)
93
+            count += 1  # One more in the stack
94
+        db.session.commit()
95
+
96
+        flash('Imported {0} ({1} duplicates) QSOs from {2}'.format(count, duplicates, filename), 'info')
97
+    else:
98
+        return render_template('tools/adif_import.jinja2', pcfg=pcfg, form=form, flash='Error with the file')
99
+
100
+    return redirect(url_for('bp_qsos.logbook', username=current_user.name))
101
+
102
+
103
+@bp_tools.route('/tools/adif/export', methods=['GET'])
104
+@login_required
105
+@check_default_profile
106
+def adif_export():
107
+    pcfg = {"title": "Export ADIF"}
108
+    return render_template('tools/adif_export.jinja2', pcfg=pcfg)
109
+
110
+
111
+@bp_tools.route('/tools/adif/export/dl', methods=['GET'])
112
+@login_required
113
+def adif_export_dl():
114
+    logs = current_user.logs
115
+
116
+    def a(k, v):
117
+        return "<{0}:{1}>{2} ".format(str(k), len(str(v)), str(v))
118
+
119
+    def generate():
120
+        yield 'ADIF Export by AHRL\r\n'
121
+        yield '\r\n'
122
+        yield '<adif_ver:5>3.0.4\r\n'
123
+        yield '<programid:4>AHRL\r\n'
124
+        yield a('station_callsign', current_user.callsign) + '\r\n'
125
+        yield a('operator', current_user.callsign) + '\r\n'
126
+        yield '\r\n'
127
+        yield '<eoh>\r\n\r\n'
128
+
129
+        for log in logs:
130
+            counter = 0
131
+            for key in ADIF_FIELDS:
132
+                if counter == 3:
133
+                    counter = 0
134
+                    yield '\r\n'
135
+
136
+                value = getattr(log, key)
137
+                if value:
138
+                    a(key, value)
139
+                    counter += 1
140
+
141
+            yield '\r\n'
142
+            # Manual ones
143
+            if log.freq:
144
+                yield a('freq', log.freq / 1000000.0)
145
+            if log.freq_rx:
146
+                yield a('freq', log.freq_rx / 1000000.0)
147
+            if log.mode:
148
+                a('mode', log.mode.mode)
149
+            if log.time_on:
150
+                a('qso_date', log.time_on.strftime('%Y%m%d'))
151
+                a('time_on', log.time_on.strftime('%H%M%S'))
152
+            if log.klass:
153
+                a('class', log.klass)
154
+            if log.band:
155
+                a('band', log.band.name)
156
+            yield '\r\n<eor>\r\n\r\n'
157
+
158
+    return Response(stream_with_context(generate()), mimetype="text/plain",
159
+                    headers={"Content-Disposition": "attachment;filename=qsos-{0}.adi".format(current_user.name)})

+ 42
- 0
controllers/users.py View File

@@ -0,0 +1,42 @@
1
+from flask import Blueprint, render_template, request, redirect, url_for
2
+from flask.ext.security import login_required, current_user
3
+from models import db, User
4
+from forms import UserProfileForm
5
+from utils import check_default_profile
6
+
7
+bp_users = Blueprint('bp_users', __name__)
8
+
9
+
10
+@bp_users.route('/user', methods=['GET'])
11
+@login_required
12
+@check_default_profile
13
+def profile():
14
+    pcfg = {"title": "My Profile"}
15
+    user = User.query.get_or_404(current_user.id)
16
+    return render_template('users/profile.jinja2', pcfg=pcfg, user=user)
17
+
18
+
19
+@bp_users.route('/user/edit', methods=['GET', 'POST'])
20
+@login_required
21
+def edit():
22
+    pcfg = {"title": "Edit my profile"}
23
+    a = User.query.get_or_404(current_user.id)
24
+
25
+    form = UserProfileForm(request.form, a)
26
+
27
+    if form.validate_on_submit():
28
+        a.callsign = form.callsign.data
29
+        a.lastname = form.lastname.data
30
+        a.firstname = form.firstname.data
31
+        a.timezone = form.timezone.data
32
+        a.locator = form.locator.data
33
+        a.lotw_name = form.lotw_name.data
34
+        a.lotw_password = form.lotw_password.data
35
+        a.eqsl_name = form.eqsl_name.data
36
+        a.eqsl_password = form.eqsl_password.data
37
+        a.swl = form.swl.data
38
+
39
+        db.session.commit()
40
+        return redirect(url_for('bp_users.profile'))
41
+
42
+    return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=a)

+ 19
- 0
crons.py View File

@@ -0,0 +1,19 @@
1
+from __future__ import print_function
2
+from models import Log
3
+from utils import get_dxcc_from_clublog
4
+
5
+
6
+def update_qsos_without_countries(db):
7
+    updated = 0
8
+    logs = Log.query.filter(Log.country.is_(None) | Log.dxcc.is_(None) | Log.cqz.is_(None)).all()
9
+    for log in logs:
10
+        if not log.call:
11
+            continue
12
+        dxcc = get_dxcc_from_clublog(log.call)
13
+        log.dxcc = dxcc['DXCC']
14
+        log.cqz = dxcc['CQZ']
15
+        log.country = dxcc['Name']
16
+        log.cont = dxcc['Continent']
17
+        db.session.commit()
18
+        updated += 1
19
+    print("Updated {0} QSOs".format(updated))

+ 4190
- 0
dbseed.py
File diff suppressed because it is too large
View File


+ 142
- 0
forms.py View File

@@ -0,0 +1,142 @@
1
+from flask.ext.wtf import Form
2
+from wtforms import StringField, PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, \
3
+    HiddenField, BooleanField
4
+from flask_wtf.file import FileField, FileAllowed, FileRequired
5
+from wtforms.validators import DataRequired
6
+from flask_security import RegisterForm
7
+from models import db, User, Note, Cat, Mode, Band
8
+from wtforms_alchemy import model_form_factory
9
+from wtforms.ext.sqlalchemy.fields import QuerySelectField
10
+from wtforms.ext.dateutil.fields import DateTimeField
11
+from utils import dt_utc_to_user_tz
12
+import datetime
13
+import pytz
14
+
15
+BaseModelForm = model_form_factory(Form)
16
+
17
+
18
+class ModelForm(BaseModelForm):
19
+    @classmethod
20
+    def get_session(cls):
21
+        return db.session
22
+
23
+
24
+class ExtendedRegisterForm(RegisterForm):
25
+    name = StringField('Name', [DataRequired()])
26
+
27
+
28
+class UserProfileForm(ModelForm):
29
+    class Meta:
30
+        model = User
31
+
32
+    password = PasswordField('Password')
33
+    name = StringField('Name')
34
+    email = StringField('Email')
35
+
36
+    callsign = StringField('Callsign', [DataRequired()])
37
+    locator = StringField('Locator', [DataRequired()])
38
+    firstname = StringField('Firstname')
39
+    lastname = StringField('Lastname')
40
+    timezone = SelectField(choices=zip(pytz.all_timezones, pytz.all_timezones),
41
+                           label='Timezone', default='UTC')
42
+    lotw_name = StringField('LoTW Username')
43
+    lotw_password = PasswordField('LoTW Password')
44
+    eqsl_name = StringField('eQSL.cc Username')
45
+    eqsl_password = PasswordField('eQSL.cc Password')
46
+
47
+    swl = BooleanField('Are you a SWL HAM ?')
48
+
49
+    submit = SubmitField('Update profile')
50
+
51
+
52
+class NoteForm(ModelForm):
53
+    class Meta:
54
+        model = Note
55
+
56
+    cat = SelectField(choices=[
57
+        ('General', 'General'),
58
+        ('Antennas', 'Antennas'),
59
+        ('Satellites', 'Satellites')], default=['General'], label='Category')
60
+    title = StringField('Title', [DataRequired()])
61
+    note = TextAreaField('Note', [DataRequired()])
62
+
63
+    submit = SubmitField('Sauver note')
64
+
65
+
66
+def get_modes():
67
+    return Mode.query.all()
68
+
69
+
70
+def get_bands():
71
+    return Band.query.filter(Band.modes.is_(None), Band.start.is_(None)).all()
72
+
73
+
74
+def dflt_mode():
75
+    return Mode.query.filter(Mode.mode == 'SSB').first()
76
+
77
+
78
+def dflt_band():
79
+    return Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.name == '40m').first()
80
+
81
+
82
+list_of_props = [['', ''], ['AUR', 'Aurora'], ['AUE', 'Aurora-E'], ['BS', 'Back scatter'],
83
+                 ['ECH', 'EchoLink'], ['EME', 'Earth-Moon-Earth'], ['ES', 'Sporadic E'],
84
+                 ['FAI', 'Field Aligned Irregularities'], ['F2', 'F2 Reflection'],
85
+                 ['INTERNET', 'Internet-assisted'], ['ION', 'Ionoscatter'], ['IRL', 'IRLP'],
86
+                 ['MS', 'Meteor scatter'], ['RPT', 'Terrestrial or atmospheric repeater or transponder'],
87
+                 ['RS', 'Rain scatter'], ['SAT', 'Satellite'], ['TEP', 'Tras-equatorial'],
88
+                 ['TR', 'Tropospheric ducting']]
89
+
90
+
91
+def get_radios():
92
+    return Cat.query.all()
93
+
94
+
95
+def foo_bar_baz_qux():
96
+    return dt_utc_to_user_tz(datetime.datetime.utcnow())
97
+
98
+
99
+class QsoForm(Form):
100
+    date = DateTimeField('Date', default=datetime.datetime.utcnow, display_format='%d-%m-%Y')
101
+    time = DateTimeField('Time', default=foo_bar_baz_qux, display_format='%H:%M:%S')
102
+    callsign = StringField('Callsign', [DataRequired()])
103
+    mode = QuerySelectField(query_factory=get_modes, default=dflt_mode, label='Mode',
104
+                            validators=[DataRequired()], get_label='mode')
105
+    band = QuerySelectField(query_factory=get_bands, default=dflt_band, label='Band',
106
+                            validators=[DataRequired()], get_label='name')
107
+    rst_s = IntegerField('RST (S)', [DataRequired()], default=59)
108
+    rst_r = IntegerField('RST (R)', [DataRequired()], default=59)
109
+    name = StringField('Name')
110
+    location = StringField('Location')
111
+    locator = StringField('Locator')  # TODO libqth is_valid_qth
112
+    comment = StringField('Comment')
113
+    country = StringField('Country', [DataRequired()])
114
+
115
+    # Hidden
116
+    dxcc_id = HiddenField(validators=[DataRequired()])
117
+    cqz = HiddenField(validators=[DataRequired()])
118
+
119
+    # Home
120
+    propagation = SelectField(choices=list_of_props, default='', label='Propagation Mode')
121
+    iota = StringField('IOTA', )
122
+
123
+    # Station
124
+    radio = QuerySelectField(query_factory=get_radios, allow_blank=True, label='Radio', get_label='radio')
125
+    frequency = IntegerField('Frequency', [DataRequired()])
126
+
127
+    # Satellite
128
+    sat_name = StringField('Sat name')
129
+    sat_mode = StringField('Sat mode')
130
+
131
+    # QSL
132
+    qsl_sent = SelectField('Sent', choices=[['N', 'No'], ['Y', 'Yes'], ['R', 'Requested']])
133
+    qsl_method = SelectField('Method', choices=[['', 'Method'], ['D', 'Direct'], ['B', 'Bureau']])
134
+    qsl_via = StringField('Via')
135
+
136
+    submit = SubmitField('Save')
137
+
138
+
139
+class AdifParse(Form):
140
+    adif_file = FileField('File', [FileRequired(),
141
+                                   FileAllowed(['adi', 'adif'], 'Adif only !')])
142
+    submit = SubmitField('Import file')

+ 46
- 0
libjambon.py View File

@@ -0,0 +1,46 @@
1
+import math
2
+from models import Band, Mode
3
+
4
+
5
+def band_to_frequency(band, mode):
6
+    if not isinstance(band, int) or not isinstance(mode, int):
7
+        raise TypeError('Sorry but only integer')
8
+
9
+    b = Band.query.filter(Band.id == band).first()
10
+    m = Mode.query.filter(Mode.id == mode).first()
11
+
12
+    if not b or not m:
13
+        return None
14
+
15
+    frequencies = Band.query.filter(Band.lower.is_(None),
16
+                                    Band.upper.is_(None),
17
+                                    Band.name == b.name,
18
+                                    Band.modes.contains(m.mode))
19
+
20
+    if frequencies.count() <= 0:
21
+        return None
22
+    else:
23
+        return frequencies.first().start
24
+
25
+
26
+def frequency_to_band(frequency):
27
+    f = frequency
28
+    if type(f) == str:
29
+        f = int(frequency)
30
+
31
+    f_q = Band.query.filter(Band.start.is_(None),
32
+                            Band.lower < f,
33
+                            Band.upper > f).single()
34
+    return f_q.name
35
+
36
+
37
+def geo_bearing_star(bearing):
38
+    dirs = ['N', 'E', 'S', 'W']
39
+
40
+    rounded = round(bearing / 22.5) % 16
41
+    if (rounded % 4) == 0:
42
+        _dir = dirs[int(rounded / 4)]
43
+    else:
44
+        _dir = dirs[int(2 * math.floor(((math.floor(rounded / 4) + 1) % 4) / 2))]
45
+        _dir += dirs[int(1 + 2 * math.floor(rounded / 8))]
46
+    return _dir

+ 1
- 0
migrations/README View File

@@ -0,0 +1 @@
1
+Generic single-database configuration.

+ 45
- 0
migrations/alembic.ini View File

@@ -0,0 +1,45 @@
1
+# A generic, single database configuration.
2
+
3
+[alembic]
4
+# template used to generate migration files
5
+# file_template = %%(rev)s_%%(slug)s
6
+
7
+# set to 'true' to run the environment during
8
+# the 'revision' command, regardless of autogenerate
9
+# revision_environment = false
10
+
11
+
12
+# Logging configuration
13
+[loggers]
14
+keys = root,sqlalchemy,alembic
15
+
16
+[handlers]
17
+keys = console
18
+
19
+[formatters]
20
+keys = generic
21
+
22
+[logger_root]
23
+level = WARN
24
+handlers = console
25
+qualname =
26
+
27
+[logger_sqlalchemy]
28
+level = WARN
29
+handlers =
30
+qualname = sqlalchemy.engine
31
+
32
+[logger_alembic]
33
+level = INFO
34
+handlers =
35
+qualname = alembic
36
+
37
+[handler_console]
38
+class = StreamHandler
39
+args = (sys.stderr,)
40
+level = NOTSET
41
+formatter = generic
42
+
43
+[formatter_generic]
44
+format = %(levelname)-5.5s [%(name)s] %(message)s
45
+datefmt = %H:%M:%S

+ 87
- 0
migrations/env.py View File

@@ -0,0 +1,87 @@
1
+from __future__ import with_statement
2
+from alembic import context
3
+from sqlalchemy import engine_from_config, pool
4
+from logging.config import fileConfig
5
+import logging
6
+
7
+# this is the Alembic Config object, which provides
8
+# access to the values within the .ini file in use.
9
+config = context.config
10
+
11
+# Interpret the config file for Python logging.
12
+# This line sets up loggers basically.
13
+fileConfig(config.config_file_name)
14
+logger = logging.getLogger('alembic.env')
15
+
16
+# add your model's MetaData object here
17
+# for 'autogenerate' support
18
+# from myapp import mymodel
19
+# target_metadata = mymodel.Base.metadata
20
+from flask import current_app
21
+config.set_main_option('sqlalchemy.url',
22
+                       current_app.config.get('SQLALCHEMY_DATABASE_URI'))
23
+target_metadata = current_app.extensions['migrate'].db.metadata
24
+
25
+# other values from the config, defined by the needs of env.py,
26
+# can be acquired:
27
+# my_important_option = config.get_main_option("my_important_option")
28
+# ... etc.
29
+
30
+
31
+def run_migrations_offline():
32
+    """Run migrations in 'offline' mode.
33
+
34
+    This configures the context with just a URL
35
+    and not an Engine, though an Engine is acceptable
36
+    here as well.  By skipping the Engine creation
37
+    we don't even need a DBAPI to be available.
38
+
39
+    Calls to context.execute() here emit the given string to the
40
+    script output.
41
+
42
+    """
43
+    url = config.get_main_option("sqlalchemy.url")
44
+    context.configure(url=url)
45
+
46
+    with context.begin_transaction():
47
+        context.run_migrations()
48
+
49
+
50
+def run_migrations_online():
51
+    """Run migrations in 'online' mode.
52
+
53
+    In this scenario we need to create an Engine
54
+    and associate a connection with the context.
55
+
56
+    """
57
+
58
+    # this callback is used to prevent an auto-migration from being generated
59
+    # when there are no changes to the schema
60
+    # reference: http://alembic.readthedocs.org/en/latest/cookbook.html
61
+    def process_revision_directives(context, revision, directives):
62
+        if getattr(config.cmd_opts, 'autogenerate', False):
63
+            script = directives[0]
64
+            if script.upgrade_ops.is_empty():
65
+                directives[:] = []
66
+                logger.info('No changes in schema detected.')
67
+
68
+    engine = engine_from_config(config.get_section(config.config_ini_section),
69
+                                prefix='sqlalchemy.',
70
+                                poolclass=pool.NullPool)
71
+
72
+    connection = engine.connect()
73
+    context.configure(connection=connection,
74
+                      target_metadata=target_metadata,
75
+                      process_revision_directives=process_revision_directives,
76
+                      **current_app.extensions['migrate'].configure_args)
77
+
78
+    try:
79
+        with context.begin_transaction():
80
+            context.run_migrations()
81
+    finally:
82
+        connection.close()
83
+
84
+if context.is_offline_mode():
85
+    run_migrations_offline()
86
+else:
87
+    run_migrations_online()

+ 22
- 0
migrations/script.py.mako View File

@@ -0,0 +1,22 @@
1
+"""${message}
2
+
3
+Revision ID: ${up_revision}
4
+Revises: ${down_revision}
5
+Create Date: ${create_date}
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = ${repr(up_revision)}
11
+down_revision = ${repr(down_revision)}
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+${imports if imports else ""}
16
+
17
+def upgrade():
18
+    ${upgrades if upgrades else "pass"}
19
+
20
+
21
+def downgrade():
22
+    ${downgrades if downgrades else "pass"}

+ 328
- 0
migrations/versions/a8a7c1e2e3a_.py View File

@@ -0,0 +1,328 @@
1
+"""empty message
2
+
3
+Revision ID: a8a7c1e2e3a
4
+Revises: None
5
+Create Date: 2016-05-05 17:01:42.609143
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'a8a7c1e2e3a'
11
+down_revision = None
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    ### commands auto generated by Alembic - please adjust! ###
19
+    op.create_table('bands',
20
+    sa.Column('id', sa.Integer(), nullable=False),
21
+    sa.Column('modes', sa.String(length=255), nullable=True),
22
+    sa.Column('name', sa.String(length=255), nullable=False),
23
+    sa.Column('lower', sa.Integer(), nullable=True),
24
+    sa.Column('upper', sa.Integer(), nullable=True),
25
+    sa.Column('start', sa.Integer(), nullable=True),
26
+    sa.PrimaryKeyConstraint('id')
27
+    )
28
+    op.create_table('cat',
29
+    sa.Column('id', sa.Integer(), nullable=False),
30
+    sa.Column('radio', sa.String(length=250), nullable=False),
31
+    sa.Column('frequency', sa.Integer(), nullable=False),
32
+    sa.Column('mode', sa.String(length=10), nullable=False),
33
+    sa.Column('timestamp', sa.DateTime(), server_default=sa.text(u'CURRENT_TIMESTAMP'), nullable=False),
34
+    sa.PrimaryKeyConstraint('id')
35
+    )
36
+    op.create_table('config',
37
+    sa.Column('id', sa.Integer(), nullable=False),
38
+    sa.Column('lotw_download_url', sa.String(length=255), nullable=True),
39
+    sa.Column('lotw_upload_url', sa.String(length=255), nullable=True),
40
+    sa.Column('lotw_rcvd_mark', sa.String(length=255), nullable=True),
41
+    sa.Column('lotw_login_url', sa.String(length=255), nullable=True),
42
+    sa.Column('eqsl_download_url', sa.String(length=255), nullable=True),
43
+    sa.Column('eqsl_rcvd_mark', sa.String(length=255), nullable=True),
44
+    sa.PrimaryKeyConstraint('id')
45
+    )
46
+    op.create_table('contest_template',
47
+    sa.Column('id', sa.Integer(), nullable=False),
48
+    sa.Column('name', sa.String(length=255), nullable=False),
49
+    sa.Column('band_160', sa.String(length=20), nullable=False),
50
+    sa.Column('band_80', sa.String(length=20), nullable=False),
51
+    sa.Column('band_40', sa.String(length=20), nullable=False),
52
+    sa.Column('band_20', sa.String(length=20), nullable=False),
53
+    sa.Column('band_15', sa.String(length=20), nullable=False),
54
+    sa.Column('band_10', sa.String(length=20), nullable=False),
55
+    sa.Column('band_6m', sa.String(length=20), nullable=False),
56
+    sa.Column('band_4m', sa.String(length=20), nullable=False),
57
+    sa.Column('band_2m', sa.String(length=20), nullable=False),
58
+    sa.Column('band_70cm', sa.String(length=20), nullable=False),
59
+    sa.Column('band_23cm', sa.String(length=20), nullable=False),
60
+    sa.Column('mode_ssb', sa.String(length=20), nullable=False),
61
+    sa.Column('mode_cw', sa.String(length=20), nullable=False),
62
+    sa.Column('serial', sa.String(length=20), nullable=False),
63
+    sa.Column('point_per_km', sa.String(length=20), nullable=False),
64
+    sa.Column('qra', sa.String(length=20), nullable=False),
65
+    sa.Column('other_exch', sa.String(length=255), nullable=False),
66
+    sa.Column('scoring', sa.String(length=255), nullable=False),
67
+    sa.PrimaryKeyConstraint('id')
68
+    )
69
+    op.create_index(op.f('ix_contest_template_name'), 'contest_template', ['name'], unique=False)
70
+    op.create_table('contests',
71
+    sa.Column('id', sa.Integer(), nullable=False),
72
+    sa.Column('name', sa.String(length=255), nullable=False),
73
+    sa.Column('start', sa.DateTime(), nullable=False),
74
+    sa.Column('end', sa.DateTime(), nullable=False),
75
+    sa.Column('template', sa.Integer(), nullable=False),
76
+    sa.Column('serial_num', sa.Integer(), nullable=False),
77
+    sa.PrimaryKeyConstraint('id')
78
+    )
79
+    op.create_table('dxcc',
80
+    sa.Column('id', sa.Integer(), nullable=False),
81
+    sa.Column('prefix', sa.String(length=10), nullable=False),
82
+    sa.Column('name', sa.String(length=150), nullable=True),
83
+    sa.Column('cqz', sa.Float(), nullable=False),
84
+    sa.Column('ituz', sa.Float(), nullable=False),
85
+    sa.Column('cont', sa.String(length=5), nullable=False),
86
+    sa.Column('long', sa.Float(), nullable=False),
87
+    sa.Column('lat', sa.Float(), nullable=False),
88
+    sa.PrimaryKeyConstraint('id')
89
+    )
90
+    op.create_index(op.f('ix_dxcc_prefix'), 'dxcc', ['prefix'], unique=False)
91
+    op.create_table('dxccexceptions',
92
+    sa.Column('id', sa.Integer(), nullable=False),
93
+    sa.Column('prefix', sa.String(length=10), nullable=False),
94
+    sa.Column('name', sa.String(length=150), nullable=True),
95
+    sa.Column('cqz', sa.Float(), nullable=False),
96
+    sa.Column('ituz', sa.Float(), nullable=False),
97
+    sa.Column('cont', sa.String(length=5), nullable=False),
98
+    sa.Column('long', sa.Float(), nullable=False),
99
+    sa.Column('lat', sa.Float(), nullable=False),
100
+    sa.Column('start', sa.DateTime(), nullable=False),
101
+    sa.Column('end', sa.DateTime(), nullable=False),
102
+    sa.PrimaryKeyConstraint('id')
103
+    )
104
+    op.create_index(op.f('ix_dxccexceptions_prefix'), 'dxccexceptions', ['prefix'], unique=False)
105
+    op.create_table('modes',
106
+    sa.Column('id', sa.Integer(), nullable=False),
107
+    sa.Column('mode', sa.String(length=255), nullable=False),
108
+    sa.Column('submode', sa.String(length=255), nullable=True),
109
+    sa.PrimaryKeyConstraint('id')
110
+    )
111
+    op.create_table('role',
112
+    sa.Column('id', sa.Integer(), nullable=False),
113
+    sa.Column('name', sa.String(length=80), nullable=False),
114
+    sa.Column('description', sa.String(length=255), nullable=True),
115
+    sa.PrimaryKeyConstraint('id'),
116
+    sa.UniqueConstraint('name')
117
+    )
118
+    op.create_table('user',
119
+    sa.Column('id', sa.Integer(), nullable=False),
120
+    sa.Column('email', sa.String(length=255), nullable=False),
121
+    sa.Column('name', sa.String(length=255), nullable=False),
122
+    sa.Column('password', sa.String(length=255), nullable=False),
123
+    sa.Column('active', sa.Boolean(), nullable=True),
124
+    sa.Column('confirmed_at', sa.DateTime(), nullable=True),
125
+    sa.Column('callsign', sa.String(length=32), nullable=True),
126
+    sa.Column('locator', sa.String(length=16), nullable=True),
127
+    sa.Column('firstname', sa.String(length=32), nullable=True),
128
+    sa.Column('lastname', sa.String(length=32), nullable=True),
129
+    sa.Column('lotw_name', sa.String(length=32), nullable=True),
130
+    sa.Column('lotw_password', sa.String(length=255), nullable=True),
131
+    sa.Column('eqsl_name', sa.String(length=32), nullable=True),
132
+    sa.Column('eqsl_password', sa.String(length=255), nullable=True),
133
+    sa.Column('timezone', sa.String(length=255), nullable=False),
134
+    sa.Column('swl', sa.Boolean(), nullable=False),
135
+    sa.PrimaryKeyConstraint('id'),
136
+    sa.UniqueConstraint('email'),
137
+    sa.UniqueConstraint('name')
138
+    )
139
+    op.create_table('apitoken',
140
+    sa.Column('id', sa.Integer(), nullable=False),
141
+    sa.Column('user_id', sa.Integer(), nullable=False),
142
+    sa.Column('token', sa.String(), nullable=False),
143
+    sa.Column('secret', sa.String(), nullable=False),
144
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
145
+    sa.PrimaryKeyConstraint('id'),
146
+    sa.UniqueConstraint('secret'),
147
+    sa.UniqueConstraint('token')
148
+    )
149
+    op.create_table('log',
150
+    sa.Column('id', sa.Integer(), nullable=False),
151
+    sa.Column('address', sa.String(length=255), nullable=True),
152
+    sa.Column('age', sa.Integer(), nullable=True),
153
+    sa.Column('a_index', sa.Float(), nullable=True),
154
+    sa.Column('ant_az', sa.Float(), nullable=True),
155
+    sa.Column('ant_el', sa.Float(), nullable=True),
156
+    sa.Column('ant_path', sa.String(length=2), nullable=True),
157
+    sa.Column('arrl_sect', sa.String(length=10), nullable=True),
158
+    sa.Column('band_id', sa.Integer(), nullable=False),
159
+    sa.Column('band_rx', sa.String(length=10), nullable=True),
160
+    sa.Column('biography', sa.Text(), nullable=True),
161
+    sa.Column('call', sa.String(length=32), nullable=True),
162
+    sa.Column('check', sa.String(length=8), nullable=True),
163
+    sa.Column('klass', sa.String(length=8), nullable=True),
164
+    sa.Column('cnty', sa.String(length=32), nullable=True),
165
+    sa.Column('comment', sa.Text(), nullable=True),
166
+    sa.Column('cont', sa.String(length=6), nullable=True),
167
+    sa.Column('contacted_op', sa.String(length=32), nullable=True),
168
+    sa.Column('contest_id', sa.String(length=32), nullable=True),
169
+    sa.Column('country', sa.String(length=64), nullable=True),
170
+    sa.Column('cqz', sa.Integer(), nullable=True),
171
+    sa.Column('distance', sa.Float(), nullable=True),
172
+    sa.Column('dxcc', sa.String(length=6), nullable=True),
173
+    sa.Column('email', sa.String(length=255), nullable=True),
174
+    sa.Column('eq_call', sa.String(length=32), nullable=True),
175
+    sa.Column('eqsl_qslrdate', sa.DateTime(), nullable=True),
176
+    sa.Column('eqsl_qslsdate', sa.DateTime(), nullable=True),
177
+    sa.Column('eqsl_qsl_rcvd', sa.String(length=2), nullable=True),
178
+    sa.Column('eqsl_qsl_sent', sa.String(length=2), nullable=True),
179
+    sa.Column('esql_status', sa.String(length=255), nullable=True),
180
+    sa.Column('force_init', sa.Integer(), nullable=True),
181
+    sa.Column('freq', sa.Integer(), nullable=True),
182
+    sa.Column('freq_rx', sa.Integer(), nullable=True),
183
+    sa.Column('gridsquare', sa.String(length=12), nullable=True),
184
+    sa.Column('heading', sa.Float(), nullable=True),
185
+    sa.Column('iota', sa.String(length=10), nullable=True),
186
+    sa.Column('ituz', sa.Integer(), nullable=True),
187
+    sa.Column('k_index', sa.Float(), nullable=True),
188
+    sa.Column('lat', sa.Float(), nullable=True),
189
+    sa.Column('lon', sa.Float(), nullable=True),
190
+    sa.Column('lotw_qslrdate', sa.DateTime(), nullable=True),
191
+    sa.Column('lotw_qslsdate', sa.DateTime(), nullable=True),
192
+    sa.Column('lotw_qsl_rcvd', sa.String(length=2), nullable=True),
193
+    sa.Column('lotw_qsl_sent', sa.String(length=2), nullable=True),
194
+    sa.Column('lotw_status', sa.String(length=255), nullable=True),
195
+    sa.Column('max_bursts', sa.Integer(), nullable=True),
196
+    sa.Column('mode_id', sa.Integer(), nullable=False),
197
+    sa.Column('ms_shower', sa.String(length=32), nullable=True),
198
+    sa.Column('my_city', sa.String(length=32), nullable=True),
199
+    sa.Column('my_cnty', sa.String(length=32), nullable=True),
200
+    sa.Column('my_country', sa.String(length=64), nullable=True),
201
+    sa.Column('my_cq_zone', sa.Integer(), nullable=True),
202
+    sa.Column('my_gridsquare', sa.String(length=12), nullable=True),
203
+    sa.Column('my_iota', sa.String(length=10), nullable=True),
204
+    sa.Column('my_itu_zone', sa.String(length=11), nullable=True),
205
+    sa.Column('my_lat', sa.Float(), nullable=True),
206
+    sa.Column('my_lon', sa.Float(), nullable=True),
207
+    sa.Column('my_name', sa.String(length=255), nullable=True),
208
+    sa.Column('my_postal_code', sa.String(length=24), nullable=True),
209
+    sa.Column('my_rig', sa.String(length=255), nullable=True),
210
+    sa.Column('my_sig', sa.String(length=32), nullable=True),
211
+    sa.Column('my_sig_info', sa.String(length=64), nullable=True),
212
+    sa.Column('my_state', sa.String(length=32), nullable=True),
213
+    sa.Column('my_street', sa.String(length=64), nullable=True),
214
+    sa.Column('name', sa.String(length=128), nullable=True),
215
+    sa.Column('notes', sa.Text(), nullable=True),
216
+    sa.Column('nr_bursts', sa.Integer(), nullable=True),
217
+    sa.Column('nr_pings', sa.Integer(), nullable=True),
218
+    sa.Column('operator', sa.String(length=32), nullable=True),
219
+    sa.Column('owner_callsign', sa.String(length=32), nullable=True),
220
+    sa.Column('pfx', sa.String(length=32), nullable=True),
221
+    sa.Column('precedence', sa.String(length=32), nullable=True),
222
+    sa.Column('prop_mode', sa.String(length=8), nullable=True),
223
+    sa.Column('public_key', sa.String(length=255), nullable=True),
224
+    sa.Column('qslmsg', sa.String(length=255), nullable=True),
225
+    sa.Column('qslrdate', sa.DateTime(), nullable=True),
226
+    sa.Column('qslsdate', sa.DateTime(), nullable=True),
227
+    sa.Column('qsl_rcvd', sa.String(length=2), nullable=True),
228
+    sa.Column('qsl_rcvd_via', sa.String(length=2), nullable=True),
229
+    sa.Column('qsl_sent', sa.String(length=2), nullable=True),
230
+    sa.Column('qsl_sent_via', sa.String(length=2), nullable=True),
231
+    sa.Column('qsl_via', sa.String(length=64), nullable=True),
232
+    sa.Column('qso_complete', sa.String(length=6), nullable=True),
233
+    sa.Column('qso_random', sa.String(length=11), nullable=True),
234
+    sa.Column('qth', sa.String(length=64), nullable=True),
235
+    sa.Column('rig', sa.String(length=255), nullable=True),
236
+    sa.Column('rst_rcvd', sa.String(length=32), nullable=True),
237
+    sa.Column('rst_sent', sa.String(length=32), nullable=True),
238
+    sa.Column('rx_pwr', sa.Float(), nullable=True),
239
+    sa.Column('sat_mode', sa.String(length=32), nullable=True),
240
+    sa.Column('sat_name', sa.String(length=32), nullable=True),
241
+    sa.Column('sfi', sa.Float(), nullable=True),
242
+    sa.Column('sig', sa.String(length=32), nullable=True),
243
+    sa.Column('sig_info', sa.String(length=64), nullable=True),
244
+    sa.Column('srx', sa.String(length=11), nullable=True),
245
+    sa.Column('srx_string', sa.String(length=32), nullable=True),
246
+    sa.Column('state', sa.String(length=32), nullable=True),
247
+    sa.Column('station_callsign', sa.String(length=32), nullable=True),
248
+    sa.Column('stx', sa.String(length=11), nullable=True),
249
+    sa.Column('stx_info', sa.String(length=32), nullable=True),
250
+    sa.Column('swl', sa.Integer(), nullable=True),
251
+    sa.Column('ten_ten', sa.Integer(), nullable=True),
252
+    sa.Column('time_off', sa.DateTime(), nullable=True),
253
+    sa.Column('time_on', sa.DateTime(), nullable=True),
254
+    sa.Column('tx_pwr', sa.Float(), nullable=True),
255
+    sa.Column('web', sa.String(length=255), nullable=True),
256
+    sa.Column('user_defined_0', sa.String(length=64), nullable=True),
257
+    sa.Column('user_defined_1', sa.String(length=64), nullable=True),
258
+    sa.Column('user_defined_2', sa.String(length=64), nullable=True),
259
+    sa.Column('user_defined_3', sa.String(length=64), nullable=True),
260
+    sa.Column('user_defined_4', sa.String(length=64), nullable=True),
261
+    sa.Column('user_defined_5', sa.String(length=64), nullable=True),
262
+    sa.Column('user_defined_6', sa.String(length=64), nullable=True),
263
+    sa.Column('user_defined_7', sa.String(length=64), nullable=True),
264
+    sa.Column('user_defined_8', sa.String(length=64), nullable=True),
265
+    sa.Column('user_defined_9', sa.String(length=64), nullable=True),
266
+    sa.Column('credit_granted', sa.String(length=64), nullable=True),
267
+    sa.Column('credit_submitted', sa.String(length=64), nullable=True),
268
+    sa.Column('user_id', sa.Integer(), nullable=False),
269
+    sa.ForeignKeyConstraint(['band_id'], ['bands.id'], ),
270
+    sa.ForeignKeyConstraint(['mode_id'], ['modes.id'], ),
271
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
272
+    sa.PrimaryKeyConstraint('id')
273
+    )
274
+    op.create_index(op.f('ix_log_call'), 'log', ['call'], unique=False)
275
+    op.create_index(op.f('ix_log_cont'), 'log', ['cont'], unique=False)
276
+    op.create_index(op.f('ix_log_dxcc'), 'log', ['dxcc'], unique=False)
277
+    op.create_index(op.f('ix_log_iota'), 'log', ['iota'], unique=False)
278
+    op.create_index(op.f('ix_log_pfx'), 'log', ['pfx'], unique=False)
279
+    op.create_index(op.f('ix_log_sat_mode'), 'log', ['sat_mode'], unique=False)
280
+    op.create_index(op.f('ix_log_sat_name'), 'log', ['sat_name'], unique=False)
281
+    op.create_index(op.f('ix_log_time_on'), 'log', ['time_on'], unique=False)
282
+    op.create_table('notes',
283
+    sa.Column('id', sa.Integer(), nullable=False),
284
+    sa.Column('cat', sa.String(length=255), nullable=False),
285
+    sa.Column('title', sa.String(length=255), nullable=False),
286
+    sa.Column('note', sa.Text(), nullable=False),
287
+    sa.Column('timestamp', sa.DateTime(), server_default=sa.text(u'CURRENT_TIMESTAMP'), nullable=True),
288
+    sa.Column('user_id', sa.Integer(), nullable=False),
289
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
290
+    sa.PrimaryKeyConstraint('id')
291
+    )
292
+    op.create_table('roles_users',
293
+    sa.Column('user_id', sa.Integer(), nullable=True),
294
+    sa.Column('role_id', sa.Integer(), nullable=True),
295
+    sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
296
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
297
+    )
298
+    ### end Alembic commands ###
299
+
300
+
301
+def downgrade():
302
+    ### commands auto generated by Alembic - please adjust! ###
303
+    op.drop_table('roles_users')
304
+    op.drop_table('notes')
305
+    op.drop_index(op.f('ix_log_time_on'), table_name='log')
306
+    op.drop_index(op.f('ix_log_sat_name'), table_name='log')
307
+    op.drop_index(op.f('ix_log_sat_mode'), table_name='log')
308
+    op.drop_index(op.f('ix_log_pfx'), table_name='log')
309
+    op.drop_index(op.f('ix_log_iota'), table_name='log')
310
+    op.drop_index(op.f('ix_log_dxcc'), table_name='log')
311
+    op.drop_index(op.f('ix_log_cont'), table_name='log')
312
+    op.drop_index(op.f('ix_log_call'), table_name='log')
313
+    op.drop_table('log')
314
+    op.drop_table('apitoken')
315
+    op.drop_table('user')
316
+    op.drop_table('role')
317
+    op.drop_table('modes')
318
+    op.drop_index(op.f('ix_dxccexceptions_prefix'), table_name='dxccexceptions')
319
+    op.drop_table('dxccexceptions')
320
+    op.drop_index(op.f('ix_dxcc_prefix'), table_name='dxcc')
321
+    op.drop_table('dxcc')
322
+    op.drop_table('contests')
323
+    op.drop_index(op.f('ix_contest_template_name'), table_name='contest_template')
324
+    op.drop_table('contest_template')
325
+    op.drop_table('config')
326
+    op.drop_table('cat')
327
+    op.drop_table('bands')
328
+    ### end Alembic commands ###

+ 394
- 0
models.py View File

@@ -0,0 +1,394 @@
1
+from flask.ext.sqlalchemy import SQLAlchemy
2
+from flask.ext.security import SQLAlchemyUserDatastore, UserMixin, RoleMixin
3
+
4
+from sqlalchemy.sql import func
5
+from sqlalchemy_searchable import make_searchable
6
+
7
+from libqth import is_valid_qth, qth_to_coords, coords_to_qth
8
+from geohelper import distance
9
+
10
+import datetime
11
+
12
+db = SQLAlchemy()
13
+make_searchable()
14
+
15
+
16
+roles_users = db.Table('roles_users',
17
+                       db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
18
+                       db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
19
+
20
+
21
+class Role(db.Model, RoleMixin):
22
+    id = db.Column(db.Integer(), primary_key=True)
23
+    name = db.Column(db.String(80), unique=True, nullable=False, info={'label': 'Name'})
24
+    description = db.Column(db.String(255), info={'label': 'Description'})
25
+
26
+    __mapper_args__ = {"order_by": name}
27
+
28
+
29
+class User(db.Model, UserMixin):
30
+    id = db.Column(db.Integer, primary_key=True)
31
+    email = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Email'})
32
+    name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
33
+    password = db.Column(db.String(255), nullable=False, info={'label': 'Password'})
34
+    active = db.Column(db.Boolean())
35
+    confirmed_at = db.Column(db.DateTime())
36
+
37
+    callsign = db.Column(db.String(32))
38
+    locator = db.Column(db.String(16))
39
+    firstname = db.Column(db.String(32))
40
+    lastname = db.Column(db.String(32))
41
+    lotw_name = db.Column(db.String(32))
42
+    lotw_password = db.Column(db.String(255))
43
+    eqsl_name = db.Column(db.String(32))
44
+    eqsl_password = db.Column(db.String(255))
45
+    timezone = db.Column(db.String(255), nullable=False)  # Managed and fed by pytz
46
+    swl = db.Column(db.Boolean(), nullable=False, default=False)
47
+
48
+    roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
49
+    logs = db.relationship('Log', backref='user', lazy='dynamic')
50
+    notes = db.relationship('Note', backref='user', lazy='dynamic')
51
+    apitokens = db.relationship('Apitoken', backref='user', lazy='dynamic')
52
+
53
+    __mapper_args__ = {"order_by": name}
54
+
55
+    def join_roles(self, string):
56
+        return string.join([i.description for i in self.roles])
57
+
58
+    def qth_to_coords(self):
59
+        qth = is_valid_qth(self.locator, 6)
60
+        if not qth:
61
+            return None
62
+        qth = qth_to_coords(self.locator, 6)
63
+        if not qth:
64
+            return None
65
+        return qth
66
+
67
+    # Give a cute name <3 More like "name - callsign" or "callsign"
68
+    def cutename(self):
69
+        cute = ""
70
+        if self.name:
71
+            cute += self.name
72
+            cute += " - "
73
+        cute += self.callsign
74
+        return cute
75
+
76
+
77
+class Apitoken(db.Model):
78
+    id = db.Column(db.Integer, primary_key=True)
79
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
80
+    token = db.Column(db.String(), unique=True, nullable=False, info={'label': 'Token'})
81
+    secret = db.Column(db.String(), unique=True, nullable=False, info={'label': 'Secret'})
82
+
83
+
84
+user_datastore = SQLAlchemyUserDatastore(db, User, Role)
85
+
86
+
87
+class Cat(db.Model):
88
+    __tablename__ = "cat"
89
+
90
+    id = db.Column(db.Integer, primary_key=True)
91
+    radio = db.Column(db.String(250), nullable=False)
92
+    frequency = db.Column(db.Integer(), nullable=False)
93
+    mode = db.Column(db.String(10), nullable=False)
94
+    timestamp = db.Column(db.DateTime(timezone=False), nullable=False, server_default=func.now(), onupdate=func.now())
95
+
96
+
97
+class Config(db.Model):
98
+    __tablename__ = "config"
99
+
100
+    id = db.Column(db.Integer, primary_key=True)
101
+    lotw_download_url = db.Column(db.String(255), default=None)
102
+    lotw_upload_url = db.Column(db.String(255), default=None)
103
+    lotw_rcvd_mark = db.Column(db.String(255), default=None)
104
+    lotw_login_url = db.Column(db.String(255), default=None)
105
+    eqsl_download_url = db.Column(db.String(255), default=None)
106
+    eqsl_rcvd_mark = db.Column(db.String(255), default=None)
107
+
108
+
109
+class ContestTemplate(db.Model):
110
+    __tablename__ = "contest_template"
111
+
112
+    id = db.Column(db.Integer, primary_key=True)
113
+    name = db.Column(db.String(255), nullable=False, index=True)
114
+    band_160 = db.Column(db.String(20), nullable=False)
115
+    band_80 = db.Column(db.String(20), nullable=False)
116
+    band_40 = db.Column(db.String(20), nullable=False)
117
+    band_20 = db.Column(db.String(20), nullable=False)
118
+    band_15 = db.Column(db.String(20), nullable=False)
119
+    band_10 = db.Column(db.String(20), nullable=False)
120
+    band_6m = db.Column(db.String(20), nullable=False)
121
+    band_4m = db.Column(db.String(20), nullable=False)
122
+    band_2m = db.Column(db.String(20), nullable=False)
123
+    band_70cm = db.Column(db.String(20), nullable=False)
124
+    band_23cm = db.Column(db.String(20), nullable=False)
125
+    mode_ssb = db.Column(db.String(20), nullable=False)
126
+    mode_cw = db.Column(db.String(20), nullable=False)
127
+    serial = db.Column(db.String(20), nullable=False)
128
+    point_per_km = db.Column(db.String(20), nullable=False)
129
+    qra = db.Column(db.String(20), nullable=False)
130
+    other_exch = db.Column(db.String(255), nullable=False)
131
+    scoring = db.Column(db.String(255), nullable=False)
132
+
133
+
134
+class Contest(db.Model):
135
+    __tablename__ = "contests"
136
+
137
+    id = db.Column(db.Integer, primary_key=True)
138
+    name = db.Column(db.String(255), nullable=False)
139
+    start = db.Column(db.DateTime(timezone=False), nullable=False)
140
+    end = db.Column(db.DateTime(timezone=False), nullable=False)
141
+    template = db.Column(db.Integer(), nullable=False)
142
+    serial_num = db.Column(db.Integer(), nullable=False)
143
+    # possible FK template->contest_template
144
+
145
+
146
+class Dxcc(db.Model):
147
+    __tablename__ = "dxcc"
148
+
149
+    id = db.Column(db.Integer, primary_key=True)
150
+    prefix = db.Column(db.String(10), nullable=False, index=True)
151
+    name = db.Column(db.String(150), default=None)
152
+    cqz = db.Column(db.Float, nullable=False)
153
+    ituz = db.Column(db.Float, nullable=False)
154
+    cont = db.Column(db.String(5), nullable=False)
155
+    long = db.Column(db.Float, nullable=False)
156
+    lat = db.Column(db.Float, nullable=False)
157
+
158
+
159
+class DxccException(db.Model):
160
+    __tablename__ = "dxccexceptions"
161
+
162
+    id = db.Column(db.Integer, primary_key=True)
163
+    prefix = db.Column(db.String(10), nullable=False, index=True)
164
+    name = db.Column(db.String(150), default=None)
165
+    cqz = db.Column(db.Float, nullable=False)
166
+    ituz = db.Column(db.Float, nullable=False)
167
+    cont = db.Column(db.String(5), nullable=False)
168
+    long = db.Column(db.Float, nullable=False)
169
+    lat = db.Column(db.Float, nullable=False)
170
+
171
+    # 0000-00-00 00:00:00 date is invalid for DateTime
172
+    # So instead we EPOCH them
173
+    start = db.Column(db.DateTime(timezone=False), nullable=False)
174
+    end = db.Column(db.DateTime(timezone=False), nullable=False)
175
+
176
+
177
+class Log(db.Model):
178
+    __tablename__ = "log"
179
+
180
+    id = db.Column(db.Integer, primary_key=True)
181
+    address = db.Column(db.String(255), default=None)
182
+    age = db.Column(db.Integer, default=None)
183
+    a_index = db.Column(db.Float, default=None)
184
+    ant_az = db.Column(db.Float, default=None)
185
+    ant_el = db.Column(db.Float, default=None)
186
+    ant_path = db.Column(db.String(2), default=None)
187
+    arrl_sect = db.Column(db.String(10), default=None)
188
+
189
+    band_id = db.Column(db.Integer(), db.ForeignKey('bands.id'), nullable=False)
190
+
191
+    band_rx = db.Column(db.String(10), default=None)
192
+    biography = db.Column(db.Text)
193
+    call = db.Column(db.String(32), default=None, index=True)
194
+    check = db.Column(db.String(8), default=None)
195
+    klass = db.Column(db.String(8), default=None)
196
+    cnty = db.Column(db.String(32), default=None)
197
+    comment = db.Column(db.Text)
198
+    cont = db.Column(db.String(6), default=None, index=True)
199
+    contacted_op = db.Column(db.String(32), default=None)
200
+    contest_id = db.Column(db.String(32), default=None)
201
+    country = db.Column(db.String(64), default=None)
202
+    cqz = db.Column(db.Integer, default=None)
203
+    distance = db.Column(db.Float, default=None)
204
+    dxcc = db.Column(db.String(6), default=None, index=True)
205
+    email = db.Column(db.String(255), default=None)
206
+    eq_call = db.Column(db.String(32), default=None)
207
+    eqsl_qslrdate = db.Column(db.DateTime(timezone=False), default=None)
208
+    eqsl_qslsdate = db.Column(db.DateTime(timezone=False), default=None)
209
+    eqsl_qsl_rcvd = db.Column(db.String(2), default=None)
210
+    eqsl_qsl_sent = db.Column(db.String(2), default=None)
211
+    esql_status = db.Column(db.String(255), default=None)
212
+    force_init = db.Column(db.Integer, default=None)
213
+    freq = db.Column(db.Integer, default=None)
214
+    freq_rx = db.Column(db.Integer, default=None)
215
+    gridsquare = db.Column(db.String(12), default=None)
216
+    heading = db.Column(db.Float, default=None)
217
+    iota = db.Column(db.String(10), default=None, index=True)
218
+    ituz = db.Column(db.Integer, default=None)
219
+    k_index = db.Column(db.Float, default=None)
220
+    lat = db.Column(db.Float, default=None)
221
+    lon = db.Column(db.Float, default=None)
222
+    lotw_qslrdate = db.Column(db.DateTime(timezone=False), default=None)
223
+    lotw_qslsdate = db.Column(db.DateTime(timezone=False), default=None)
224
+    lotw_qsl_rcvd = db.Column(db.String(2), default=None)
225
+    lotw_qsl_sent = db.Column(db.String(2), default=None)
226
+    lotw_status = db.Column(db.String(255), default=None)
227
+    max_bursts = db.Column(db.Integer, default=None)
228
+
229
+    mode_id = db.Column(db.Integer(), db.ForeignKey('modes.id'), nullable=False)
230
+
231
+    ms_shower = db.Column(db.String(32), default=None)
232
+    my_city = db.Column(db.String(32), default=None)
233
+    my_cnty = db.Column(db.String(32), default=None)
234
+    my_country = db.Column(db.String(64), default=None)
235
+    my_cq_zone = db.Column(db.Integer, default=None)
236
+    my_gridsquare = db.Column(db.String(12), default=None)
237
+    my_iota = db.Column(db.String(10), default=None)
238
+    my_itu_zone = db.Column(db.String(11), default=None)
239
+    my_lat = db.Column(db.Float, default=None)
240
+    my_lon = db.Column(db.Float, default=None)
241
+    my_name = db.Column(db.String(255), default=None)
242
+    my_postal_code = db.Column(db.String(24), default=None)
243
+    my_rig = db.Column(db.String(255), default=None)
244
+    my_sig = db.Column(db.String(32), default=None)
245
+    my_sig_info = db.Column(db.String(64), default=None)
246
+    my_state = db.Column(db.String(32), default=None)
247
+    my_street = db.Column(db.String(64), default=None)
248
+    name = db.Column(db.String(128), default=None)
249
+    notes = db.Column(db.Text, default=None)
250
+    nr_bursts = db.Column(db.Integer, default=None)
251
+    nr_pings = db.Column(db.Integer, default=None)
252
+    operator = db.Column(db.String(32), default=None)
253
+    owner_callsign = db.Column(db.String(32), default=None)
254
+    pfx = db.Column(db.String(32), default=None, index=True)
255
+    precedence = db.Column(db.String(32), default=None)
256
+    prop_mode = db.Column(db.String(8), default=None)
257
+    public_key = db.Column(db.String(255), default=None)
258
+    qslmsg = db.Column(db.String(255), default=None)
259
+    qslrdate = db.Column(db.DateTime(timezone=False), default=None)
260
+    qslsdate = db.Column(db.DateTime(timezone=False), default=None)
261
+    qsl_rcvd = db.Column(db.String(2), default=None)
262
+    qsl_rcvd_via = db.Column(db.String(2), default=None)
263
+    qsl_sent = db.Column(db.String(2), default=None)
264
+    qsl_sent_via = db.Column(db.String(2), default=None)
265
+    qsl_via = db.Column(db.String(64), default=None)
266
+    qso_complete = db.Column(db.String(6), default=None)
267
+    qso_random = db.Column(db.String(11), default=None)
268
+    qth = db.Column(db.String(64), default=None)
269
+    rig = db.Column(db.String(255), default=None)
270
+    rst_rcvd = db.Column(db.String(32), default=None)
271
+    rst_sent = db.Column(db.String(32), default=None)
272
+    rx_pwr = db.Column(db.Float, default=None)
273
+    sat_mode = db.Column(db.String(32), default=None, index=True)
274
+    sat_name = db.Column(db.String(32), default=None, index=True)
275
+    sfi = db.Column(db.Float, default=None)
276
+    sig = db.Column(db.String(32), default=None)
277
+    sig_info = db.Column(db.String(64), default=None)
278
+    srx = db.Column(db.String(11), default=None)
279
+    srx_string = db.Column(db.String(32), default=None)
280
+    state = db.Column(db.String(32), default=None)
281
+    station_callsign = db.Column(db.String(32), default=None)
282
+    stx = db.Column(db.String(11), default=None)
283
+    stx_info = db.Column(db.String(32), default=None)
284
+    swl = db.Column(db.Integer, default=None)
285
+    ten_ten = db.Column(db.Integer, default=None)
286
+    time_off = db.Column(db.DateTime(timezone=False), default=None)
287
+    time_on = db.Column(db.DateTime(timezone=False), default=None, index=True)
288
+    tx_pwr = db.Column(db.Float, default=None)
289
+    web = db.Column(db.String(255), default=None)
290
+    user_defined_0 = db.Column(db.String(64), default=None)
291
+    user_defined_1 = db.Column(db.String(64), default=None)
292
+    user_defined_2 = db.Column(db.String(64), default=None)
293
+    user_defined_3 = db.Column(db.String(64), default=None)
294
+    user_defined_4 = db.Column(db.String(64), default=None)
295
+    user_defined_5 = db.Column(db.String(64), default=None)
296
+    user_defined_6 = db.Column(db.String(64), default=None)
297
+    user_defined_7 = db.Column(db.String(64), default=None)
298
+    user_defined_8 = db.Column(db.String(64), default=None)
299
+    user_defined_9 = db.Column(db.String(64), default=None)
300
+    credit_granted = db.Column(db.String(64), default=None)
301
+    credit_submitted = db.Column(db.String(64), default=None)
302
+
303
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
304
+
305
+    __mapper_args__ = {"order_by": time_on.desc()}
306
+
307
+    # Give a cute name <3 More like "name - callsign" or "callsign"
308
+    def cutename(self):
309
+        cute = ""
310
+        if self.name:
311
+            cute += self.name
312
+            cute += " - "
313
+        cute += self.call
314
+        return cute
315
+
316
+    def country_grid_coords(self):
317
+        if 'sqlite' in db.engine.driver:
318
+            q = Dxcc.query.filter(Dxcc.prefix == func.substr(self.call, 1, func.LENGTH(Dxcc.prefix))).order_by(
319
+                func.length(Dxcc.prefix).desc()).limit(1)
320
+        else:
321
+            q = Dxcc.query.filter(Dxcc.prefix == func.substring(self.call, 1, func.LENGTH(Dxcc.prefix))).order_by(
322
+                func.length(Dxcc.prefix).desc()).limit(1)
323
+        if q.count() <= 0:
324
+            return None
325
+        else:
326
+            return {'latitude': q[0].lat, 'longitude': q[0].long}
327
+
328
+    def country_grid(self):
329
+        q = self.country_grid_coords()
330
+        return coords_to_qth(q['latitude'], q['longitude'], 6)['qth']
331
+
332
+    def distance_from_user(self):
333
+        if not self.gridsquare:
334
+            qso_gs = self.country_grid()
335
+        else:
336
+            qso_gs = self.gridsquare
337
+
338
+        if not qso_gs or not self.user.locator:
339
+            return None
340
+
341
+        if not is_valid_qth(self.user.locator, 6) or not is_valid_qth(qso_gs, 6):
342
+            return None
343
+
344
+        _f = qth_to_coords(self.user.locator, 6)  # precision, latitude, longitude
345
+        _t = qth_to_coords(qso_gs, 6)  # precision, latitude, longitude
346
+
347
+        return distance.haversine_km(_f['latitude'],
348
+                                     _f['longitude'],
349
+                                     _t['latitude'],
350
+                                     _t['longitude'])
351
+
352
+
353
+class Note(db.Model):
354
+    __tablename__ = "notes"
355
+
356
+    id = db.Column(db.Integer, primary_key=True)
357
+    cat = db.Column(db.String(255), nullable=False)
358
+    title = db.Column(db.String(255), nullable=False)
359
+    note = db.Column(db.Text, nullable=False)
360
+    timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
361
+
362
+    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
363
+
364
+    __mapper_args__ = {"order_by": id.desc()}
365
+
366
+    def timestamp_tz(self):
367
+        t = self.timestamp
368
+        if self.user.timezone.offset < 0:
369
+            return t - datetime.timedelta(hours=self.user.timezone.offset)
370
+        else:
371
+            return t + datetime.timedelta(hours=self.user.timezone.offset)
372
+
373
+
374
+class Mode(db.Model):
375
+    __tablename__ = "modes"
376
+
377
+    id = db.Column(db.Integer, primary_key=True)
378
+    mode = db.Column(db.String(255), nullable=False)
379
+    submode = db.Column(db.String(255), nullable=True)  # Unused as now
380
+
381
+    logs = db.relationship('Log', backref='mode', lazy='dynamic')
382
+
383
+
384
+class Band(db.Model):
385
+    __tablename__ = "bands"
386
+
387
+    id = db.Column(db.Integer, primary_key=True)
388
+    modes = db.Column(db.String(255), nullable=True)
389
+    name = db.Column(db.String(255), nullable=False)
390
+    lower = db.Column(db.Integer, nullable=True)
391
+    upper = db.Column(db.Integer, nullable=True)
392
+    start = db.Column(db.Integer, nullable=True)
393
+
394
+    logs = db.relationship('Log', backref='band', lazy='dynamic')

+ 587
- 0
static/css/bootstrap-theme.css View File

@@ -0,0 +1,587 @@
1
+/*!
2
+ * Bootstrap v3.3.6 (http://getbootstrap.com)
3
+ * Copyright 2011-2015 Twitter, Inc.
4
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5
+ */
6
+.btn-default,
7
+.btn-primary,
8
+.btn-success,
9
+.btn-info,
10
+.btn-warning,
11
+.btn-danger {
12
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
13
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
14
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15
+}
16
+.btn-default:active,
17
+.btn-primary:active,
18
+.btn-success:active,
19
+.btn-info:active,
20
+.btn-warning:active,
21
+.btn-danger:active,
22
+.btn-default.active,
23
+.btn-primary.active,
24
+.btn-success.active,
25
+.btn-info.active,
26
+.btn-warning.active,
27
+.btn-danger.active {
28
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
29
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30
+}
31
+.btn-default.disabled,
32
+.btn-primary.disabled,
33
+.btn-success.disabled,
34
+.btn-info.disabled,
35
+.btn-warning.disabled,
36
+.btn-danger.disabled,
37
+.btn-default[disabled],
38
+.btn-primary[disabled],
39
+.btn-success[disabled],
40
+.btn-info[disabled],
41
+.btn-warning[disabled],
42
+.btn-danger[disabled],
43
+fieldset[disabled] .btn-default,
44
+fieldset[disabled] .btn-primary,
45
+fieldset[disabled] .btn-success,
46
+fieldset[disabled] .btn-info,
47
+fieldset[disabled] .btn-warning,
48
+fieldset[disabled] .btn-danger {
49
+  -webkit-box-shadow: none;
50
+          box-shadow: none;
51
+}
52
+.btn-default .badge,
53
+.btn-primary .badge,
54
+.btn-success .badge,
55
+.btn-info .badge,
56
+.btn-warning .badge,
57
+.btn-danger .badge {
58
+  text-shadow: none;
59
+}
60
+.btn:active,
61
+.btn.active {
62
+  background-image: none;
63
+}
64
+.btn-default {
65
+  text-shadow: 0 1px 0 #fff;
66
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
67
+  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
68
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
69
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
70
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
71
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
72
+  background-repeat: repeat-x;
73
+  border-color: #dbdbdb;
74
+  border-color: #ccc;
75
+}
76
+.btn-default:hover,
77
+.btn-default:focus {
78
+  background-color: #e0e0e0;
79
+  background-position: 0 -15px;
80
+}
81
+.btn-default:active,
82
+.btn-default.active {
83
+  background-color: #e0e0e0;
84
+  border-color: #dbdbdb;
85
+}
86
+.btn-default.disabled,
87
+.btn-default[disabled],
88
+fieldset[disabled] .btn-default,
89
+.btn-default.disabled:hover,
90
+.btn-default[disabled]:hover,
91
+fieldset[disabled] .btn-default:hover,
92
+.btn-default.disabled:focus,
93
+.btn-default[disabled]:focus,
94
+fieldset[disabled] .btn-default:focus,
95
+.btn-default.disabled.focus,
96
+.btn-default[disabled].focus,
97
+fieldset[disabled] .btn-default.focus,
98
+.btn-default.disabled:active,
99
+.btn-default[disabled]:active,
100
+fieldset[disabled] .btn-default:active,
101
+.btn-default.disabled.active,
102
+.btn-default[disabled].active,
103
+fieldset[disabled] .btn-default.active {
104
+  background-color: #e0e0e0;
105
+  background-image: none;
106
+}
107
+.btn-primary {
108
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
109
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
110
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
111
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
112
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
113
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
114
+  background-repeat: repeat-x;
115
+  border-color: #245580;
116
+}
117
+.btn-primary:hover,
118
+.btn-primary:focus {
119
+  background-color: #265a88;
120
+  background-position: 0 -15px;
121
+}
122
+.btn-primary:active,
123
+.btn-primary.active {
124
+  background-color: #265a88;
125
+  border-color: #245580;
126
+}
127
+.btn-primary.disabled,
128
+.btn-primary[disabled],
129
+fieldset[disabled] .btn-primary,
130
+.btn-primary.disabled:hover,
131
+.btn-primary[disabled]:hover,
132
+fieldset[disabled] .btn-primary:hover,
133
+.btn-primary.disabled:focus,
134
+.btn-primary[disabled]:focus,
135
+fieldset[disabled] .btn-primary:focus,
136
+.btn-primary.disabled.focus,
137
+.btn-primary[disabled].focus,
138
+fieldset[disabled] .btn-primary.focus,
139
+.btn-primary.disabled:active,
140
+.btn-primary[disabled]:active,
141
+fieldset[disabled] .btn-primary:active,
142
+.btn-primary.disabled.active,
143
+.btn-primary[disabled].active,
144
+fieldset[disabled] .btn-primary.active {
145
+  background-color: #265a88;
146
+  background-image: none;
147
+}
148
+.btn-success {
149
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
150
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
151
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
152
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
153
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
154
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
155
+  background-repeat: repeat-x;
156
+  border-color: #3e8f3e;
157
+}
158
+.btn-success:hover,
159
+.btn-success:focus {
160
+  background-color: #419641;
161
+  background-position: 0 -15px;
162
+}
163
+.btn-success:active,
164
+.btn-success.active {
165
+  background-color: #419641;
166
+  border-color: #3e8f3e;
167
+}
168
+.btn-success.disabled,
169
+.btn-success[disabled],
170
+fieldset[disabled] .btn-success,
171
+.btn-success.disabled:hover,
172
+.btn-success[disabled]:hover,
173
+fieldset[disabled] .btn-success:hover,
174
+.btn-success.disabled:focus,
175
+.btn-success[disabled]:focus,
176
+fieldset[disabled] .btn-success:focus,
177
+.btn-success.disabled.focus,
178
+.btn-success[disabled].focus,
179
+fieldset[disabled] .btn-success.focus,
180
+.btn-success.disabled:active,
181
+.btn-success[disabled]:active,
182
+fieldset[disabled] .btn-success:active,
183
+.btn-success.disabled.active,
184
+.btn-success[disabled].active,
185
+fieldset[disabled] .btn-success.active {
186
+  background-color: #419641;
187
+  background-image: none;
188
+}
189
+.btn-info {
190
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
191
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
192
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
193
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
194
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
195
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
196
+  background-repeat: repeat-x;
197
+  border-color: #28a4c9;
198
+}
199
+.btn-info:hover,
200
+.btn-info:focus {
201
+  background-color: #2aabd2;
202
+  background-position: 0 -15px;
203
+}
204
+.btn-info:active,
205
+.btn-info.active {
206
+  background-color: #2aabd2;
207
+  border-color: #28a4c9;
208
+}
209
+.btn-info.disabled,
210
+.btn-info[disabled],
211
+fieldset[disabled] .btn-info,
212
+.btn-info.disabled:hover,
213
+.btn-info[disabled]:hover,
214
+fieldset[disabled] .btn-info:hover,
215
+.btn-info.disabled:focus,
216
+.btn-info[disabled]:focus,
217
+fieldset[disabled] .btn-info:focus,
218
+.btn-info.disabled.focus,
219
+.btn-info[disabled].focus,
220
+fieldset[disabled] .btn-info.focus,
221
+.btn-info.disabled:active,
222
+.btn-info[disabled]:active,
223
+fieldset[disabled] .btn-info:active,
224
+.btn-info.disabled.active,
225
+.btn-info[disabled].active,
226
+fieldset[disabled] .btn-info.active {
227
+  background-color: #2aabd2;
228
+  background-image: none;
229
+}
230
+.btn-warning {
231
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
232
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
233
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
234
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
235
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
236
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
237
+  background-repeat: repeat-x;
238
+  border-color: #e38d13;
239
+}
240
+.btn-warning:hover,
241
+.btn-warning:focus {
242
+  background-color: #eb9316;
243
+  background-position: 0 -15px;
244
+}
245
+.btn-warning:active,
246
+.btn-warning.active {
247
+  background-color: #eb9316;
248
+  border-color: #e38d13;
249
+}
250
+.btn-warning.disabled,
251
+.btn-warning[disabled],
252
+fieldset[disabled] .btn-warning,
253
+.btn-warning.disabled:hover,
254
+.btn-warning[disabled]:hover,
255
+fieldset[disabled] .btn-warning:hover,
256
+.btn-warning.disabled:focus,
257
+.btn-warning[disabled]:focus,
258
+fieldset[disabled] .btn-warning:focus,
259
+.btn-warning.disabled.focus,
260
+.btn-warning[disabled].focus,
261
+fieldset[disabled] .btn-warning.focus,
262
+.btn-warning.disabled:active,
263
+.btn-warning[disabled]:active,
264
+fieldset[disabled] .btn-warning:active,
265
+.btn-warning.disabled.active,
266
+.btn-warning[disabled].active,
267
+fieldset[disabled] .btn-warning.active {
268
+  background-color: #eb9316;
269
+  background-image: none;
270
+}
271
+.btn-danger {
272
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
273
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
274
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
275
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
276
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
277
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
278
+  background-repeat: repeat-x;
279
+  border-color: #b92c28;
280
+}
281
+.btn-danger:hover,
282
+.btn-danger:focus {
283
+  background-color: #c12e2a;
284
+  background-position: 0 -15px;
285
+}
286
+.btn-danger:active,
287
+.btn-danger.active {
288
+  background-color: #c12e2a;
289
+  border-color: #b92c28;
290
+}
291
+.btn-danger.disabled,
292
+.btn-danger[disabled],
293
+fieldset[disabled] .btn-danger,
294
+.btn-danger.disabled:hover,
295
+.btn-danger[disabled]:hover,
296
+fieldset[disabled] .btn-danger:hover,
297
+.btn-danger.disabled:focus,
298
+.btn-danger[disabled]:focus,
299
+fieldset[disabled] .btn-danger:focus,
300
+.btn-danger.disabled.focus,
301
+.btn-danger[disabled].focus,
302
+fieldset[disabled] .btn-danger.focus,
303
+.btn-danger.disabled:active,
304
+.btn-danger[disabled]:active,
305
+fieldset[disabled] .btn-danger:active,
306
+.btn-danger.disabled.active,
307
+.btn-danger[disabled].active,
308
+fieldset[disabled] .btn-danger.active {
309
+  background-color: #c12e2a;
310
+  background-image: none;
311
+}
312
+.thumbnail,
313
+.img-thumbnail {
314
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
315
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
316
+}
317
+.dropdown-menu > li > a:hover,
318
+.dropdown-menu > li > a:focus {
319
+  background-color: #e8e8e8;
320
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
321
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
322
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
323
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
324
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
325
+  background-repeat: repeat-x;
326
+}
327
+.dropdown-menu > .active > a,
328
+.dropdown-menu > .active > a:hover,
329
+.dropdown-menu > .active > a:focus {
330
+  background-color: #2e6da4;
331
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
332
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
333
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
334
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
335
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
336
+  background-repeat: repeat-x;
337
+}
338
+.navbar-default {
339
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
340
+  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
341
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
342
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
343
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
344
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
345
+  background-repeat: repeat-x;
346
+  border-radius: 4px;
347
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
348
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
349
+}
350
+.navbar-default .navbar-nav > .open > a,
351
+.navbar-default .navbar-nav > .active > a {
352
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
353
+  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
354
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
355
+  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
356
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
357
+  background-repeat: repeat-x;
358
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
359
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
360
+}