Browse Source

Reformat with Black

pull/9/head
Dashie der otter 9 months ago
parent
commit
6464af08f7
Signed by: Dashie <dashie@sigpipe.me> GPG Key ID: C2D57B325840B755
50 changed files with 2484 additions and 2114 deletions
  1. 8
    8
      adif.py
  2. 27
    17
      ahrl.py
  3. 24
    27
      app.py
  4. 9
    9
      controllers/admin.py
  5. 5
    5
      controllers/api.py
  6. 63
    56
      controllers/contacts.py
  7. 7
    7
      controllers/extapi.py
  8. 36
    29
      controllers/logbooks.py
  9. 13
    8
      controllers/main.py
  10. 31
    21
      controllers/notes.py
  11. 533
    394
      controllers/qsos.py
  12. 119
    108
      controllers/tools.py
  13. 38
    21
      controllers/users.py
  14. 106
    103
      crons.py
  15. 137
    134
      dbseed.py
  16. 164
    157
      forms.py
  17. 191
    103
      libjambon.py
  18. 17
    14
      migrations/env.py
  19. 305
    291
      migrations/versions/001_33200cef9060.py
  20. 16
    15
      migrations/versions/002_1981d67bd949.py
  21. 20
    19
      migrations/versions/003_dba2c36b8d15.py
  22. 16
    15
      migrations/versions/004_85dc2959dc8c.py
  23. 55
    52
      migrations/versions/005_b68e04878516.py
  24. 30
    28
      migrations/versions/006_d1c8faa24092.py
  25. 6
    6
      migrations/versions/007_f4d4e3c42eb7.py
  26. 98
    102
      migrations/versions/008_488cd2ea543d.py
  27. 6
    6
      migrations/versions/009_790b9af160b5.py
  28. 2
    2
      migrations/versions/010_516077ddda8d.py
  29. 70
    45
      migrations/versions/011_8ddd19e5391a.py
  30. 4
    4
      migrations/versions/012_0222deffc1dd.py
  31. 8
    8
      migrations/versions/013_a69619a8fd47.py
  32. 4
    4
      migrations/versions/014_229e1f2ce062.py
  33. 26
    24
      migrations/versions/015_4669be672bc4.py
  34. 4
    4
      migrations/versions/016_71ebbd5fe6de.py
  35. 7
    6
      migrations/versions/017_eb1dd834c778.py
  36. 6
    6
      migrations/versions/018_1cfbdbdced17.py
  37. 6
    6
      migrations/versions/019_4b8ff69cf574.py
  38. 6
    6
      migrations/versions/020_96dd40d300ed.py
  39. 4
    4
      migrations/versions/021_b71a316c6504.py
  40. 38
    38
      migrations/versions/022_98ea6ef6fc14.py
  41. 26
    9
      migrations/versions/024_5539ae4bd72a.py
  42. 8
    8
      migrations/versions/025_df93b7d9aa63.py
  43. 4
    4
      migrations/versions/026_eb8858fd3d9c.py
  44. 4
    4
      migrations/versions/027_e072762b7964_.py
  45. 107
    106
      models.py
  46. 1
    1
      tests/__init__.py
  47. 29
    33
      tests/test_app.py
  48. 17
    16
      tests/test_libjambon.py
  49. 2
    1
      tests/test_utils.py
  50. 21
    20
      utils.py

+ 8
- 8
adif.py View File

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

+ 27
- 17
ahrl.py View File

@@ -6,14 +6,21 @@ import texttable
6 6
 from flask_debugtoolbar import DebugToolbarExtension
7 7
 from flask_migrate import MigrateCommand
8 8
 from flask_script import Manager
9
-from crons import update_qsos_without_countries, update_dxcc_from_cty_xml, \
10
-    populate_logs_gridsquare_cache, cron_sync_eqsl, update_qsos_from_hamqth, cron_sync_from_eqsl
9
+from crons import (
10
+    update_qsos_without_countries,
11
+    update_dxcc_from_cty_xml,
12
+    populate_logs_gridsquare_cache,
13
+    cron_sync_eqsl,
14
+    update_qsos_from_hamqth,
15
+    cron_sync_from_eqsl,
16
+)
11 17
 from dbseed import make_db_seed
12 18
 from models import db
13 19
 
14 20
 try:
15 21
     from raven.contrib.flask import Sentry
16 22
     import raven
23
+
17 24
     print(" * Sentry support loaded")
18 25
     HAS_SENTRY = True
19 26
 except ImportError as e:
@@ -25,10 +32,10 @@ from app import create_app
25 32
 app = create_app()
26 33
 
27 34
 if HAS_SENTRY:
28
-    app.config['SENTRY_RELEASE'] = raven.fetch_git_sha(os.path.dirname(__file__))
29
-    sentry = Sentry(app, dsn=app.config['SENTRY_DSN'])
35
+    app.config["SENTRY_RELEASE"] = raven.fetch_git_sha(os.path.dirname(__file__))
36
+    sentry = Sentry(app, dsn=app.config["SENTRY_DSN"])
30 37
     print(" * Sentry support activated")
31
-    print(" * Sentry DSN: %s" % app.config['SENTRY_DSN'])
38
+    print(" * Sentry DSN: %s" % app.config["SENTRY_DSN"])
32 39
 
33 40
 toolbar = DebugToolbarExtension(app)
34 41
 manager = Manager(app)
@@ -40,14 +47,14 @@ def routes():
40 47
     """Dump all routes of defined app"""
41 48
     table = texttable.Texttable()
42 49
     table.set_deco(texttable.Texttable().HEADER)
43
-    table.set_cols_dtype(['t', 't', 't'])
50
+    table.set_cols_dtype(["t", "t", "t"])
44 51
     table.set_cols_align(["l", "l", "l"])
45 52
     table.set_cols_width([60, 30, 90])
46 53
 
47 54
     table.add_rows([["Prefix", "Verb", "URI Pattern"]])
48 55
 
49 56
     for rule in sorted(app.url_map.iter_rules(), key=lambda x: str(x)):
50
-        methods = ','.join(rule.methods)
57
+        methods = ",".join(rule.methods)
51 58
         table.add_row([rule.endpoint, methods, rule])
52 59
 
53 60
     print(table.draw())
@@ -64,8 +71,9 @@ def seed():
64 71
     """Seed database with default content"""
65 72
     make_db_seed(db)
66 73
 
67
-CacheCommand = Manager(usage='Perform cache actions')
68
-CronCommand = Manager(usage='Perform crons actions')
74
+
75
+CacheCommand = Manager(usage="Perform cache actions")
76
+CronCommand = Manager(usage="Perform crons actions")
69 77
 
70 78
 
71 79
 @CronCommand.command
@@ -85,8 +93,9 @@ def update_qsos_countries():
85 93
 
86 94
 
87 95
 @CronCommand.command
88
-@CronCommand.option('--dryrun', dest='dry_run', action='store_true', default=False,
89
-                    help="Dry run, doesn't commit anything")
96
+@CronCommand.option(
97
+    "--dryrun", dest="dry_run", action="store_true", default=False, help="Dry run, doesn't commit anything"
98
+)
90 99
 def sync_to_eqsl(dry_run=False):
91 100
     """Push to eQSL logs with requested eQSL sync"""
92 101
     print("-- STARTED on {0}".format(datetime.datetime.now()))
@@ -95,8 +104,9 @@ def sync_to_eqsl(dry_run=False):
95 104
 
96 105
 
97 106
 @CronCommand.command
98
-@CronCommand.option('--dryrun', dest='dry_run', action='store_true', default=False,
99
-                    help="Dry run, doesn't commit anything")
107
+@CronCommand.option(
108
+    "--dryrun", dest="dry_run", action="store_true", default=False, help="Dry run, doesn't commit anything"
109
+)
100 110
 def sync_from_eqsl(dry_run=False):
101 111
     """Fetch from eQSL logs """
102 112
     print("-- STARTED on {0}".format(datetime.datetime.now()))
@@ -120,11 +130,11 @@ def populate_logs_gridsquare():
120 130
     print("-- FINISHED on {0}".format(datetime.datetime.now()))
121 131
 
122 132
 
123
-manager.add_command('db', MigrateCommand)
124
-manager.add_command('cache', CacheCommand)
125
-manager.add_command('cron', CronCommand)
133
+manager.add_command("db", MigrateCommand)
134
+manager.add_command("cache", CacheCommand)
135
+manager.add_command("cron", CronCommand)
126 136
 
127
-if __name__ == '__main__':
137
+if __name__ == "__main__":
128 138
     try:
129 139
         manager.run()
130 140
     except KeyboardInterrupt as e:

+ 24
- 27
app.py View File

@@ -37,16 +37,16 @@ def create_app(cfg=None):
37 37
 
38 38
     Bootstrap(app)
39 39
 
40
-    app.jinja_env.add_extension('jinja2.ext.with_')
41
-    app.jinja_env.add_extension('jinja2.ext.do')
42
-    app.jinja_env.filters['localize'] = dt_utc_to_user_tz
43
-    app.jinja_env.filters['show_date_no_offset'] = show_date_no_offset
40
+    app.jinja_env.add_extension("jinja2.ext.with_")
41
+    app.jinja_env.add_extension("jinja2.ext.do")
42
+    app.jinja_env.filters["localize"] = dt_utc_to_user_tz
43
+    app.jinja_env.filters["show_date_no_offset"] = show_date_no_offset
44 44
     app.jinja_env.globals.update(is_admin=is_admin)
45 45
 
46 46
     # Logging
47 47
     if not app.debug:
48
-        formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')
49
-        file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), 'a', 1000000, 1)
48
+        formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]")
49
+        file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), "a", 1000000, 1)
50 50
         file_handler.setLevel(logging.DEBUG)
51 51
         file_handler.setFormatter(formatter)
52 52
         app.logger.addHandler(file_handler)
@@ -57,22 +57,21 @@ def create_app(cfg=None):
57 57
     db.init_app(app)
58 58
 
59 59
     # Setup Flask-Security
60
-    security = Security(app, user_datastore,
61
-                        register_form=ExtendedRegisterForm)
60
+    security = Security(app, user_datastore, register_form=ExtendedRegisterForm)
62 61
 
63 62
     git_version = ""
64 63
     gitpath = os.path.join(os.getcwd(), ".git")
65 64
     if os.path.isdir(gitpath):
66
-        git_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
65
+        git_version = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
67 66
         if git_version:
68
-            git_version = git_version.strip().decode('UTF-8')
67
+            git_version = git_version.strip().decode("UTF-8")
69 68
 
70 69
     @app.before_request
71 70
     def before_request():
72 71
         g.cfg = {
73
-            'AHRL_VERSION_VER': __VERSION__,
74
-            'AHRL_VERSION_GIT': git_version,
75
-            'AHRL_VERSION': "{0} ({1})".format(__VERSION__, git_version),
72
+            "AHRL_VERSION_VER": __VERSION__,
73
+            "AHRL_VERSION_GIT": git_version,
74
+            "AHRL_VERSION": "{0} ({1})".format(__VERSION__, git_version),
76 75
         }
77 76
 
78 77
     @app.errorhandler(InvalidUsage)
@@ -81,7 +80,7 @@ def create_app(cfg=None):
81 80
         response.status_code = error.status_code
82 81
         return response
83 82
 
84
-    pictures = UploadSet('pictures', IMAGES)
83
+    pictures = UploadSet("pictures", IMAGES)
85 84
     configure_uploads(app, pictures)
86 85
 
87 86
     app.register_blueprint(bp_main)
@@ -95,33 +94,31 @@ def create_app(cfg=None):
95 94
     app.register_blueprint(bp_extapi)
96 95
 
97 96
     # Used in development
98
-    @app.route('/uploads/<path:stuff>', methods=['GET'])
97
+    @app.route("/uploads/<path:stuff>", methods=["GET"])
99 98
     def get_uploads_stuff(stuff):
100
-        print("Get {0} from {1}".format(stuff, app.config['UPLOADS_DEFAULT_DEST']))
101
-        return send_from_directory(app.config['UPLOADS_DEFAULT_DEST'], stuff, as_attachment=False)
99
+        print("Get {0} from {1}".format(stuff, app.config["UPLOADS_DEFAULT_DEST"]))
100
+        return send_from_directory(app.config["UPLOADS_DEFAULT_DEST"], stuff, as_attachment=False)
102 101
 
103 102
     @app.errorhandler(404)
104 103
     def page_not_found(msg):
105
-        pcfg = {"title": "Whoops, something failed.",
106
-                "error": 404, "message": "Page not found", "e": msg}
107
-        return render_template('error_page.jinja2', pcfg=pcfg), 404
104
+        pcfg = {"title": "Whoops, something failed.", "error": 404, "message": "Page not found", "e": msg}
105
+        return render_template("error_page.jinja2", pcfg=pcfg), 404
108 106
 
109 107
     @app.errorhandler(403)
110 108
     def err_forbidden(msg):
111
-        pcfg = {"title": "Whoops, something failed.",
112
-                "error": 403, "message": "Access forbidden", "e": msg}
113
-        return render_template('error_page.jinja2', pcfg=pcfg), 403
109
+        pcfg = {"title": "Whoops, something failed.", "error": 403, "message": "Access forbidden", "e": msg}
110
+        return render_template("error_page.jinja2", pcfg=pcfg), 403
114 111
 
115 112
     @app.errorhandler(410)
116 113
     def err_gone(msg):
117
-        pcfg = {"title": "Whoops, something failed.",
118
-                "error": 410, "message": "Gone", "e": msg}
119
-        return render_template('error_page.jinja2', pcfg=pcfg), 410
114
+        pcfg = {"title": "Whoops, something failed.", "error": 410, "message": "Gone", "e": msg}
115
+        return render_template("error_page.jinja2", pcfg=pcfg), 410
120 116
 
121 117
     if not app.debug:
118
+
122 119
         @app.errorhandler(500)
123 120
         def err_failed(msg):
124 121
             pcfg = {"title": "Whoops, something failed.", "error": 500, "message": "Something is broken", "e": msg}
125
-            return render_template('error_page.jinja2', pcfg=pcfg), 500
122
+            return render_template("error_page.jinja2", pcfg=pcfg), 500
126 123
 
127 124
     return app

+ 9
- 9
controllers/admin.py View File

@@ -5,32 +5,32 @@ from forms import ConfigForm
5 5
 from models import db, Logging, Config
6 6
 from utils import check_default_profile, is_admin
7 7
 
8
-bp_admin = Blueprint('bp_admin', __name__)
8
+bp_admin = Blueprint("bp_admin", __name__)
9 9
 
10 10
 
11
-@bp_admin.route('/admin/logs', methods=['GET'])
11
+@bp_admin.route("/admin/logs", methods=["GET"])
12 12
 @login_required
13 13
 @check_default_profile
14 14
 def logs():
15 15
     if not is_admin():
16
-        return redirect(url_for('bp_home.home'))
16
+        return redirect(url_for("bp_home.home"))
17 17
     pcfg = {"title": "Application Logs"}
18 18
     _logs = Logging.query.order_by(Logging.timestamp.desc()).limit(100).all()
19
-    return render_template('admin/logs.jinja2', pcfg=pcfg, logs=_logs)
19
+    return render_template("admin/logs.jinja2", pcfg=pcfg, logs=_logs)
20 20
 
21 21
 
22
-@bp_admin.route('/admin/config', methods=['GET', 'POST'])
22
+@bp_admin.route("/admin/config", methods=["GET", "POST"])
23 23
 @login_required
24 24
 @check_default_profile
25 25
 def config():
26 26
     if not is_admin():
27
-        return redirect(url_for('bp_main.home'))
27
+        return redirect(url_for("bp_main.home"))
28 28
 
29 29
     pcfg = {"title": "Application Config"}
30 30
 
31 31
     _config = Config.query.one()
32 32
     if not _config:
33
-        flash("Config not found", 'error')
33
+        flash("Config not found", "error")
34 34
         return redirect(url_for("bp_main.home"))
35 35
 
36 36
     form = ConfigForm(request.form, obj=_config)
@@ -47,6 +47,6 @@ def config():
47 47
 
48 48
         db.session.commit()
49 49
         flash("Configuration updated", "info")
50
-        return redirect(url_for('bp_admin.config'))
50
+        return redirect(url_for("bp_admin.config"))
51 51
 
52
-    return render_template('admin/config.jinja2', pcfg=pcfg, form=form)
52
+    return render_template("admin/config.jinja2", pcfg=pcfg, form=form)

+ 5
- 5
controllers/api.py View File

@@ -4,10 +4,10 @@ from flask_security import login_required, current_user
4 4
 from models import db, Apitoken
5 5
 from utils import generate_uniques_apitoken
6 6
 
7
-bp_api = Blueprint('bp_api', __name__)
7
+bp_api = Blueprint("bp_api", __name__)
8 8
 
9 9
 
10
-@bp_api.route('/api/token/new')
10
+@bp_api.route("/api/token/new")
11 11
 @login_required
12 12
 def apitoken_new():
13 13
     apitoken = generate_uniques_apitoken()
@@ -20,10 +20,10 @@ def apitoken_new():
20 20
     a.secret = apitoken["secret"]
21 21
     db.session.add(a)
22 22
     db.session.commit()
23
-    return redirect(url_for('bp_users.user_profile'))
23
+    return redirect(url_for("bp_users.user_profile"))
24 24
 
25 25
 
26
-@bp_api.route('/api/token/<string:apit>/del')
26
+@bp_api.route("/api/token/<string:apit>/del")
27 27
 @login_required
28 28
 def apitoken_del(apit):
29 29
     apitoken = Apitoken.query.filter(Apitoken.id == apit).first()
@@ -33,4 +33,4 @@ def apitoken_del(apit):
33 33
 
34 34
     db.session.delete(apitoken)
35 35
     db.session.commit()
36
-    return redirect(url_for('bp_users.user_profile'))
36
+    return redirect(url_for("bp_users.user_profile"))

+ 63
- 56
controllers/contacts.py View File

@@ -10,21 +10,26 @@ from libjambon import geo_bearing_star
10 10
 from models import db, Contact, User, Logbook, Log
11 11
 from utils import check_default_profile, InvalidUsage
12 12
 
13
-bp_contacts = Blueprint('bp_contacts', __name__)
13
+bp_contacts = Blueprint("bp_contacts", __name__)
14 14
 
15 15
 
16
-@bp_contacts.route('/contacts', methods=['GET'])
16
+@bp_contacts.route("/contacts", methods=["GET"])
17 17
 @login_required
18 18
 @check_default_profile
19 19
 def contacts():
20 20
     pcfg = {"title": "My contacts"}
21 21
     _contacts = Contact.query.filter(Contact.user_id == current_user.id).all()
22
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
23
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
24
-    return render_template('contacts/view.jinja2', pcfg=pcfg, contacts=_contacts, logbooks=logbooks)
22
+    logbooks = (
23
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
24
+        .join(Log)
25
+        .filter(Logbook.user_id == current_user.id)
26
+        .group_by(Logbook.id)
27
+        .all()
28
+    )
29
+    return render_template("contacts/view.jinja2", pcfg=pcfg, contacts=_contacts, logbooks=logbooks)
25 30
 
26 31
 
27
-@bp_contacts.route('/contacts/<int:contact_id>/edit', methods=['GET', 'POST'])
32
+@bp_contacts.route("/contacts/<int:contact_id>/edit", methods=["GET", "POST"])
28 33
 @login_required
29 34
 @check_default_profile
30 35
 def edit(contact_id):
@@ -41,33 +46,39 @@ def edit(contact_id):
41 46
         a.gridsquare = form.gridsquare.data
42 47
 
43 48
         if not current_user.locator or not form.gridsquare.data:
44
-            flash('Missing locator_qso or locator_user', 'error')
49
+            flash("Missing locator_qso or locator_user", "error")
45 50
             return redirect(url_for("bp_contacts.contacts"))
46 51
 
47 52
         if not is_valid_qth(current_user.locator, 6) or not is_valid_qth(form.gridsquare.data, 6):
48
-            flash('One of the supplied QTH is not valid', 'error')
53
+            flash("One of the supplied QTH is not valid", "error")
49 54
             return redirect(url_for("bp_contacts.contacts"))
50 55
 
51 56
         _f = qth_to_coords(current_user.locator, 6)  # precision, latitude, longitude
52 57
         _t = qth_to_coords(form.gridsquare.data, 6)  # precision, latitude, longitude
53 58
 
54
-        a.latitude = _t['latitude']
55
-        a.longitude = _t['longitude']
56
-        a.distance = distance.haversine_km(_f['latitude'], _f['longitude'], _t['latitude'], _t['longitude'])
57
-        a.bearing = bearing.initial_compass_bearing(_f['latitude'], _f['longitude'], _t['latitude'], _t['longitude'])
59
+        a.latitude = _t["latitude"]
60
+        a.longitude = _t["longitude"]
61
+        a.distance = distance.haversine_km(_f["latitude"], _f["longitude"], _t["latitude"], _t["longitude"])
62
+        a.bearing = bearing.initial_compass_bearing(_f["latitude"], _f["longitude"], _t["latitude"], _t["longitude"])
58 63
         a.bearing_star = geo_bearing_star(a.bearing)
59 64
 
60 65
         db.session.commit()
61
-        flash("Success saving contact: {0}".format(a.callsign), 'success')
62
-        return redirect(url_for('bp_contacts.contacts'))
66
+        flash("Success saving contact: {0}".format(a.callsign), "success")
67
+        return redirect(url_for("bp_contacts.contacts"))
63 68
 
64
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
65
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
66
-    return render_template('contacts/edit.jinja2', pcfg=pcfg, form=form, contact=a,
67
-                           contact_id=contact_id, logbooks=logbooks)
69
+    logbooks = (
70
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
71
+        .join(Log)
72
+        .filter(Logbook.user_id == current_user.id)
73
+        .group_by(Logbook.id)
74
+        .all()
75
+    )
76
+    return render_template(
77
+        "contacts/edit.jinja2", pcfg=pcfg, form=form, contact=a, contact_id=contact_id, logbooks=logbooks
78
+    )
68 79
 
69 80
 
70
-@bp_contacts.route('/contacts/new', methods=['GET', 'POST'])
81
+@bp_contacts.route("/contacts/new", methods=["GET", "POST"])
71 82
 @login_required
72 83
 @check_default_profile
73 84
 def new():
@@ -81,35 +92,40 @@ def new():
81 92
         a.gridsquare = form.gridsquare.data
82 93
 
83 94
         if not current_user.locator or not form.gridsquare.data:
84
-            flash('Missing locator_qso or locator_user', 'error')
95
+            flash("Missing locator_qso or locator_user", "error")
85 96
             return redirect(url_for("bp_contacts.contacts"))
86 97
 
87 98
         if not is_valid_qth(current_user.locator, 6) or not is_valid_qth(form.gridsquare.data, 6):
88
-            flash('One of the supplied QTH is not valid', 'error')
99
+            flash("One of the supplied QTH is not valid", "error")
89 100
             return redirect(url_for("bp_contacts.contacts"))
90 101
 
91 102
         _f = qth_to_coords(current_user.locator, 6)  # precision, latitude, longitude
92 103
         _t = qth_to_coords(form.gridsquare.data, 6)  # precision, latitude, longitude
93 104
 
94
-        a.latitude = _t['latitude']
95
-        a.longitude = _t['longitude']
96
-        a.distance = distance.haversine_km(_f['latitude'], _f['longitude'], _t['latitude'], _t['longitude'])
97
-        a.bearing = bearing.initial_compass_bearing(_f['latitude'], _f['longitude'], _t['latitude'], _t['longitude'])
105
+        a.latitude = _t["latitude"]
106
+        a.longitude = _t["longitude"]
107
+        a.distance = distance.haversine_km(_f["latitude"], _f["longitude"], _t["latitude"], _t["longitude"])
108
+        a.bearing = bearing.initial_compass_bearing(_f["latitude"], _f["longitude"], _t["latitude"], _t["longitude"])
98 109
         a.bearing_star = geo_bearing_star(a.bearing)
99 110
 
100 111
         a.user_id = current_user.id
101 112
 
102 113
         db.session.add(a)
103 114
         db.session.commit()
104
-        flash("Success updating contact: {0}".format(a.callsign), 'success')
105
-        return redirect(url_for('bp_contacts.contacts'))
115
+        flash("Success updating contact: {0}".format(a.callsign), "success")
116
+        return redirect(url_for("bp_contacts.contacts"))
106 117
 
107
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
108
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
109
-    return render_template('contacts/new.jinja2', pcfg=pcfg, form=form, logbooks=logbooks)
118
+    logbooks = (
119
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
120
+        .join(Log)
121
+        .filter(Logbook.user_id == current_user.id)
122
+        .group_by(Logbook.id)
123
+        .all()
124
+    )
125
+    return render_template("contacts/new.jinja2", pcfg=pcfg, form=form, logbooks=logbooks)
110 126
 
111 127
 
112
-@bp_contacts.route('/contacts/<int:contact_id>/delete', methods=['GET', 'DELETE', 'PUT'])
128
+@bp_contacts.route("/contacts/<int:contact_id>/delete", methods=["GET", "DELETE", "PUT"])
113 129
 @login_required
114 130
 @check_default_profile
115 131
 def delete(contact_id):
@@ -120,43 +136,37 @@ def delete(contact_id):
120 136
 
121 137
     db.session.delete(contact)
122 138
     db.session.commit()
123
-    flash("Success deleting contact: {0}".format(contact.callsign), 'success')
124
-    return redirect(url_for('bp_contacts.contacts'))
139
+    flash("Success deleting contact: {0}".format(contact.callsign), "success")
140
+    return redirect(url_for("bp_contacts.contacts"))
125 141
 
126 142
 
127
-@bp_contacts.route('/contacts/<string:username>/geojson', methods=['GET'])
143
+@bp_contacts.route("/contacts/<string:username>/geojson", methods=["GET"])
128 144
 def contacts_geojson(username):
129 145
     if not username:
130
-        raise InvalidUsage('Missing username', status_code=400)
146
+        raise InvalidUsage("Missing username", status_code=400)
131 147
 
132 148
     user = User.query.filter(User.name == username).first()
133 149
     if not user:
134
-        raise InvalidUsage('User not found', status_code=404)
150
+        raise InvalidUsage("User not found", status_code=404)
135 151
 
136 152
     _contacts = Contact.query.filter(User.id == user.id).all()
137 153
 
138 154
     if not is_valid_qth(user.locator, 6):
139
-        raise InvalidUsage('QTH is not valid', status_code=400)
155
+        raise InvalidUsage("QTH is not valid", status_code=400)
140 156
     _u = qth_to_coords(user.locator, 6)  # precision, latitude, longitude
141 157
 
142
-    j = [{
143
-        "type": "Feature",
144
-        "properties": {
145
-            "name": user.cutename(),
146
-            "callsign": user.callsign,
147
-            "own": True,
148
-            "icon": "home"
149
-        },
150
-        "geometry": {
151
-            "type": "Point",
152
-            "coordinates": [_u['longitude'], _u['latitude']]
158
+    j = [
159
+        {
160
+            "type": "Feature",
161
+            "properties": {"name": user.cutename(), "callsign": user.callsign, "own": True, "icon": "home"},
162
+            "geometry": {"type": "Point", "coordinates": [_u["longitude"], _u["latitude"]]},
153 163
         }
154
-    }]
164
+    ]
155 165
 
156 166
     for log in _contacts:
157 167
         if log.gridsquare:
158 168
             if not is_valid_qth(log.gridsquare, 6):
159
-                raise InvalidUsage('QTH is not valid', status_code=400)
169
+                raise InvalidUsage("QTH is not valid", status_code=400)
160 170
             _f = qth_to_coords(log.gridsquare, 6)  # precision, latitude, longitude
161 171
         else:
162 172
             _f = log.country_grid_coords()
@@ -170,13 +180,10 @@ def contacts_geojson(username):
170 180
                 "distance": log.distance,
171 181
                 "bearing": log.bearing,
172 182
                 "bearing_star": log.bearing_star,
173
-                "icon": "qso"
183
+                "icon": "qso",
174 184
             },
175
-            "geometry": {
176
-                "type": "Point",
177
-                "coordinates": [_f['longitude'], _f['latitude']]
178
-            }
185
+            "geometry": {"type": "Point", "coordinates": [_f["longitude"], _f["latitude"]]},
179 186
         }
180 187
         j.append(f)
181 188
 
182
-    return Response(json.dumps(j), mimetype='application/json')
189
+    return Response(json.dumps(j), mimetype="application/json")

+ 7
- 7
controllers/extapi.py View File

@@ -3,7 +3,7 @@ from xmlrpc.server import CGIXMLRPCRequestHandler, SimpleXMLRPCDispatcher
3 3
 from adif import parse as adif_parser
4 4
 from models import db, Log, Band, Mode
5 5
 
6
-bp_extapi = Blueprint('bp_extapi', __name__)
6
+bp_extapi = Blueprint("bp_extapi", __name__)
7 7
 
8 8
 handler = SimpleXMLRPCDispatcher(allow_none=True, encoding=None)
9 9
 
@@ -22,7 +22,7 @@ handler.register_introspection_functions()
22 22
 def add_record(adif_record):
23 23
     print("Log.add_record")
24 24
     # FLDIGI send only a record, add fake end-of-header to not break parser
25
-    parsed = adif_parser(b'<eoh>' + adif_record.encode('UTF-8'))
25
+    parsed = adif_parser(b"<eoh>" + adif_record.encode("UTF-8"))
26 26
     if len(parsed) >= 1:
27 27
         parsed = parsed[0]
28 28
     """
@@ -42,19 +42,19 @@ def get_record(call):
42 42
     print("Log.get_record")
43 43
     return "xxx"
44 44
 
45
+
45 46
 handler.register_function(add_record, name="log.add_record")
46 47
 handler.register_function(check_dup, name="log.check_dup")
47 48
 handler.register_function(get_record, name="log.get_record")
48 49
 
49 50
 
50
-@bp_extapi.route('/extapi/<path:whatever>', methods=['POST', 'GET'])
51
+@bp_extapi.route("/extapi/<path:whatever>", methods=["POST", "GET"])
51 52
 def catchall(whatever):
52 53
     print("You wanted: {0}".format(whatever))
53 54
 
54 55
 
55
-@bp_extapi.route('/extapi/RPC2', methods=['POST', 'GET'])
56
+@bp_extapi.route("/extapi/RPC2", methods=["POST", "GET"])
56 57
 def endpoint():
57
-    dispatch = getattr(handler, '_dispatch', None)
58
+    dispatch = getattr(handler, "_dispatch", None)
58 59
     req = handler._marshaled_dispatch(request.data, dispatch)
59
-    return Response(req, mimetype='text/xml')
60
-
60
+    return Response(req, mimetype="text/xml")

+ 36
- 29
controllers/logbooks.py View File

@@ -6,38 +6,48 @@ from forms import LogbookForm
6 6
 from models import db, Logbook, User, Log
7 7
 from utils import check_default_profile
8 8
 
9
-bp_logbooks = Blueprint('bp_logbooks', __name__)
9
+bp_logbooks = Blueprint("bp_logbooks", __name__)
10 10
 
11 11
 
12
-@bp_logbooks.route('/logbooks/<string:user>', methods=['GET'])
12
+@bp_logbooks.route("/logbooks/<string:user>", methods=["GET"])
13 13
 @check_default_profile
14 14
 def logbooks(user):
15 15
     user = User.query.filter(User.name == user).first()
16 16
     if not user:
17
-        flash("User not found", 'error')
18
-        return redirect(url_for('bp_main.home'))
17
+        flash("User not found", "error")
18
+        return redirect(url_for("bp_main.home"))
19 19
 
20 20
     pcfg = {"title": "{0}'s logbooks".format(user.name)}
21 21
 
22 22
     if current_user.is_authenticated:
23
-        _logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
24
-            Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
23
+        _logbooks = (
24
+            db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
25
+            .join(Log)
26
+            .filter(Logbook.user_id == current_user.id)
27
+            .group_by(Logbook.id)
28
+            .all()
29
+        )
25 30
     else:
26
-        _logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
27
-            Log).filter(Logbook.user_id == user.id, Logbook.public.is_(True)).group_by(Logbook.id).all()
31
+        _logbooks = (
32
+            db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
33
+            .join(Log)
34
+            .filter(Logbook.user_id == user.id, Logbook.public.is_(True))
35
+            .group_by(Logbook.id)
36
+            .all()
37
+        )
28 38
 
29
-    return render_template('logbooks/logbooks.jinja2', pcfg=pcfg, user=user, logbooks=_logbooks)
39
+    return render_template("logbooks/logbooks.jinja2", pcfg=pcfg, user=user, logbooks=_logbooks)
30 40
 
31 41
 
32
-@bp_logbooks.route('/logbooks/<string:logbook_slug>/edit', methods=['GET', 'POST'])
42
+@bp_logbooks.route("/logbooks/<string:logbook_slug>/edit", methods=["GET", "POST"])
33 43
 @login_required
34 44
 @check_default_profile
35 45
 def edit(logbook_slug):
36 46
     pcfg = {"title": "Edit my logbooks"}
37 47
     a = Logbook.query.filter(Logbook.user_id == current_user.id, Logbook.slug == logbook_slug).first()
38 48
     if not a:
39
-        flash("Logbook not found", 'error')
40
-        return redirect(url_for('bp_logbooks.logbooks', user=current_user.name))
49
+        flash("Logbook not found", "error")
50
+        return redirect(url_for("bp_logbooks.logbooks", user=current_user.name))
41 51
 
42 52
     form = LogbookForm(request.form, obj=a)
43 53
 
@@ -62,13 +72,13 @@ def edit(logbook_slug):
62 72
             fl.default = True
63 73
 
64 74
         db.session.commit()
65
-        flash("Success saving logbook: {0}".format(a.name), 'success')
66
-        return redirect(url_for('bp_logbooks.logbooks', user=current_user.name))
75
+        flash("Success saving logbook: {0}".format(a.name), "success")
76
+        return redirect(url_for("bp_logbooks.logbooks", user=current_user.name))
67 77
 
68
-    return render_template('logbooks/edit.jinja2', pcfg=pcfg, form=form, logbook=a, logbook_slug=logbook_slug)
78
+    return render_template("logbooks/edit.jinja2", pcfg=pcfg, form=form, logbook=a, logbook_slug=logbook_slug)
69 79
 
70 80
 
71
-@bp_logbooks.route('/logbooks/new', methods=['GET', 'POST'])
81
+@bp_logbooks.route("/logbooks/new", methods=["GET", "POST"])
72 82
 @login_required
73 83
 @check_default_profile
74 84
 def new():
@@ -76,7 +86,7 @@ def new():
76 86
 
77 87
     form = LogbookForm()
78 88
 
79
-    if request.method == 'GET':
89
+    if request.method == "GET":
80 90
         form.callsign.data = current_user.callsign
81 91
         form.locator.data = current_user.locator
82 92
         form.default.data = False
@@ -95,10 +105,7 @@ def new():
95 105
         a.default = form.default.data
96 106
 
97 107
         if form.default.data:
98
-            cur_dflt = Logbook.query.filter(
99
-                Logbook.user_id == current_user.id,
100
-                Logbook.default.is_(True)
101
-            ).all()
108
+            cur_dflt = Logbook.query.filter(Logbook.user_id == current_user.id, Logbook.default.is_(True)).all()
102 109
             for l in cur_dflt:
103 110
                 l.default = False
104 111
         else:
@@ -111,24 +118,24 @@ def new():
111 118
         db.session.add(a)
112 119
         db.session.commit()
113 120
 
114
-        flash("Success updating logbook: {0}".format(a.name), 'success')
115
-        return redirect(url_for('bp_logbooks.logbooks', user=current_user.name))
121
+        flash("Success updating logbook: {0}".format(a.name), "success")
122
+        return redirect(url_for("bp_logbooks.logbooks", user=current_user.name))
116 123
 
117
-    return render_template('logbooks/new.jinja2', pcfg=pcfg, form=form)
124
+    return render_template("logbooks/new.jinja2", pcfg=pcfg, form=form)
118 125
 
119 126
 
120
-@bp_logbooks.route('/logbooks/<string:logbook_slug>/delete', methods=['GET', 'DELETE', 'PUT'])
127
+@bp_logbooks.route("/logbooks/<string:logbook_slug>/delete", methods=["GET", "DELETE", "PUT"])
121 128
 @login_required
122 129
 @check_default_profile
123 130
 def delete(logbook_slug):
124 131
     logbook = Logbook.query.filter(Logbook.user_id == current_user.id, Logbook.slug == logbook_slug).first()
125 132
     if not logbook:
126
-        flash("Logbook not found", 'error')
127
-        return redirect(url_for('bp_logbooks.logbooks', user=current_user.name))
133
+        flash("Logbook not found", "error")
134
+        return redirect(url_for("bp_logbooks.logbooks", user=current_user.name))
128 135
 
129 136
     db.session.delete(logbook)
130 137
     db.session.commit()
131 138
 
132
-    flash("Success deleting logbook: {0}".format(logbook.name), 'success')
139
+    flash("Success deleting logbook: {0}".format(logbook.name), "success")
133 140
 
134
-    return redirect(url_for('bp_logbooks.logbooks', user=current_user.name))
141
+    return redirect(url_for("bp_logbooks.logbooks", user=current_user.name))

+ 13
- 8
controllers/main.py View File

@@ -4,11 +4,11 @@ from sqlalchemy import func
4 4
 
5 5
 from models import db, User, Logbook, Log, DxccEntities, DxccExceptions, DxccPrefixes
6 6
 
7
-bp_main = Blueprint('bp_main', __name__)
7
+bp_main = Blueprint("bp_main", __name__)
8 8
 
9 9
 
10 10
 # Show public logbooks
11
-@bp_main.route('/')
11
+@bp_main.route("/")
12 12
 def home():
13 13
     # Sanity check
14 14
     _dp = db.session.query(DxccPrefixes.id).count()
@@ -19,20 +19,25 @@ def home():
19 19
     # check are lowered to avoid changing them maybe too frequently
20 20
     if _dp < 3500 or _dx < 16300 or _de < 300:
21 21
         flash("DXCC Tables are empty, check manual", "error")
22
-        #abort(500)
22
+        # abort(500)
23 23
 
24 24
     pcfg = {"title": "AHRL - Another Ham Radio Log"}
25 25
     users = User.query.all()
26 26
 
27 27
     logbooks = None
28 28
     if current_user.is_authenticated:
29
-        logbooks = db.session.query(Logbook.id, Logbook.slug, Logbook.name, func.count(Log.id)).join(
30
-            Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
29
+        logbooks = (
30
+            db.session.query(Logbook.id, Logbook.slug, Logbook.name, func.count(Log.id))
31
+            .join(Log)
32
+            .filter(Logbook.user_id == current_user.id)
33
+            .group_by(Logbook.id)
34
+            .all()
35
+        )
31 36
 
32
-    return render_template('home.jinja2', pcfg=pcfg, users=users, logbooks=logbooks)
37
+    return render_template("home.jinja2", pcfg=pcfg, users=users, logbooks=logbooks)
33 38
 
34 39
 
35
-@bp_main.route('/about')
40
+@bp_main.route("/about")
36 41
 def about():
37 42
     pcfg = {"title": "About AHRL - Another Ham Radio Log"}
38
-    return render_template('about.jinja2', pcfg=pcfg)
43
+    return render_template("about.jinja2", pcfg=pcfg)

+ 31
- 21
controllers/notes.py View File

@@ -6,23 +6,28 @@ from forms import NoteForm
6 6
 from models import db, Note, Logbook, Log
7 7
 from utils import check_default_profile
8 8
 
9
-bp_notes = Blueprint('bp_notes', __name__)
9
+bp_notes = Blueprint("bp_notes", __name__)
10 10
 
11 11
 
12
-@bp_notes.route('/notes', methods=['GET'])
12
+@bp_notes.route("/notes", methods=["GET"])
13 13
 @login_required
14 14
 @check_default_profile
15 15
 def notes():
16 16
     pcfg = {"title": "My notes"}
17 17
 
18 18
     _notes = Note.query.filter(Note.user_id == current_user.id).all()
19
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
20
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
19
+    logbooks = (
20
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
21
+        .join(Log)
22
+        .filter(Logbook.user_id == current_user.id)
23
+        .group_by(Logbook.id)
24
+        .all()
25
+    )
21 26
 
22
-    return render_template('notes/view.jinja2', pcfg=pcfg, notes=_notes, logbooks=logbooks)
27
+    return render_template("notes/view.jinja2", pcfg=pcfg, notes=_notes, logbooks=logbooks)
23 28
 
24 29
 
25
-@bp_notes.route('/notes/<int:note_id>/edit', methods=['GET', 'POST'])
30
+@bp_notes.route("/notes/<int:note_id>/edit", methods=["GET", "POST"])
26 31
 @login_required
27 32
 @check_default_profile
28 33
 def edit(note_id):
@@ -30,8 +35,8 @@ def edit(note_id):
30 35
 
31 36
     a = Note.query.filter(Note.user_id == current_user.id, Note.id == note_id).first()
32 37
     if not a:
33
-        flash("Note not found", 'error')
34
-        return redirect(url_for('bp_notes.notes'))
38
+        flash("Note not found", "error")
39
+        return redirect(url_for("bp_notes.notes"))
35 40
 
36 41
     form = NoteForm(request.form, obj=a)
37 42
 
@@ -42,15 +47,15 @@ def edit(note_id):
42 47
 
43 48
         db.session.commit()
44 49
 
45
-        flash("Success saving note: {0}".format(a.title), 'success')
46
-        return redirect(url_for('bp_notes.notes'))
50
+        flash("Success saving note: {0}".format(a.title), "success")
51
+        return redirect(url_for("bp_notes.notes"))
47 52
 
48 53
     logbooks = Logbook.query.filter(Logbook.user_id == current_user.id).all()
49 54
 
50
-    return render_template('notes/edit.jinja2', pcfg=pcfg, form=form, note=a, note_id=note_id, logbooks=logbooks)
55
+    return render_template("notes/edit.jinja2", pcfg=pcfg, form=form, note=a, note_id=note_id, logbooks=logbooks)
51 56
 
52 57
 
53
-@bp_notes.route('/notes/new', methods=['GET', 'POST'])
58
+@bp_notes.route("/notes/new", methods=["GET", "POST"])
54 59
 @login_required
55 60
 @check_default_profile
56 61
 def new():
@@ -68,24 +73,29 @@ def new():
68 73
         db.session.add(a)
69 74
         db.session.commit()
70 75
 
71
-        flash("Success updating note: {0}".format(a.title), 'success')
72
-        return redirect(url_for('bp_notes.notes'))
76
+        flash("Success updating note: {0}".format(a.title), "success")
77
+        return redirect(url_for("bp_notes.notes"))
73 78
 
74
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
75
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
76
-    return render_template('notes/new.jinja2', pcfg=pcfg, form=form, logbooks=logbooks)
79
+    logbooks = (
80
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
81
+        .join(Log)
82
+        .filter(Logbook.user_id == current_user.id)
83
+        .group_by(Logbook.id)
84
+        .all()
85
+    )
86
+    return render_template("notes/new.jinja2", pcfg=pcfg, form=form, logbooks=logbooks)
77 87
 
78 88
 
79
-@bp_notes.route('/notes/<int:note_id>/delete', methods=['GET', 'DELETE', 'PUT'])
89
+@bp_notes.route("/notes/<int:note_id>/delete", methods=["GET", "DELETE", "PUT"])
80 90
 @login_required
81 91
 @check_default_profile
82 92
 def delete(note_id):
83 93
     note = Note.query.filter(Note.user_id == current_user.id, Note.id == note_id).first()
84 94
     if not note:
85
-        flash("Note not found", 'error')
86
-        return redirect(url_for('bp_notes.notes'))
95
+        flash("Note not found", "error")
96
+        return redirect(url_for("bp_notes.notes"))
87 97
 
88 98
     db.session.delete(note)
89 99
     db.session.commit()
90 100
 
91
-    return redirect(url_for('bp_notes.notes'))
101
+    return redirect(url_for("bp_notes.notes"))

+ 533
- 394
controllers/qsos.py
File diff suppressed because it is too large
View File


+ 119
- 108
controllers/tools.py View File

@@ -12,19 +12,19 @@ from models import db, Log, Mode, User, Logbook, Band
12 12
 from utils import check_default_profile, InvalidUsage
13 13
 from libjambon import ADIF_FIELDS, coordinates2adif, adif2coordinates
14 14
 
15
-bp_tools = Blueprint('bp_tools', __name__)
15
+bp_tools = Blueprint("bp_tools", __name__)
16 16
 
17 17
 
18
-@bp_tools.route('/tools/adif/import', methods=['GET'])
18
+@bp_tools.route("/tools/adif/import", methods=["GET"])
19 19
 @login_required
20 20
 @check_default_profile
21 21
 def adif_import():
22 22
     pcfg = {"title": "Import ADIF"}
23 23
     form = AdifParse()
24
-    return render_template('tools/adif_import.jinja2', pcfg=pcfg, form=form)
24
+    return render_template("tools/adif_import.jinja2", pcfg=pcfg, form=form)
25 25
 
26 26
 
27
-@bp_tools.route('/logbooks/adif/import', methods=['POST'])
27
+@bp_tools.route("/logbooks/adif/import", methods=["POST"])
28 28
 @login_required
29 29
 @check_default_profile
30 30
 def adif_import_file():
@@ -48,12 +48,18 @@ def adif_import_file():
48 48
 
49 49
         for log in parsed_adif:
50 50
             # First check if duplicate
51
-            _date = "{0} {1}".format(log['qso_date'], log['time_on'])
51
+            _date = "{0} {1}".format(log["qso_date"], log["time_on"])
52 52
             _date_wo_tz = datetime.datetime.strptime(_date, "%Y%m%d %H%M%S")
53
-            duplicates_count = db.session.query(Log.id).filter(Log.user_id == current_user.id,
54
-                                                               Log.call == log['call'],
55
-                                                               Log.time_on == _date_wo_tz,
56
-                                                               Log.logbook_id == _logbook.id).count()
53
+            duplicates_count = (
54
+                db.session.query(Log.id)
55
+                .filter(
56
+                    Log.user_id == current_user.id,
57
+                    Log.call == log["call"],
58
+                    Log.time_on == _date_wo_tz,
59
+                    Log.logbook_id == _logbook.id,
60
+                )
61
+                .count()
62
+            )
57 63
             if duplicates_count > 0:
58 64
                 duplicates += 1
59 65
                 continue  # duplicate found, skip record
@@ -63,9 +69,9 @@ def adif_import_file():
63 69
                 if key not in log:
64 70
                     continue
65 71
 
66
-                if key == 'swl':
67
-                    val = 1 if log[key] == 'Y' else 0
68
-                elif key == 'lon' or key == 'lat':
72
+                if key == "swl":
73
+                    val = 1 if log[key] == "Y" else 0
74
+                elif key == "lon" or key == "lat":
69 75
                     val = adif2coordinates(log[key])
70 76
                 else:
71 77
                     val = log[key]
@@ -73,60 +79,56 @@ def adif_import_file():
73 79
                 setattr(l, key, val)
74 80
 
75 81
             # other fields to manage specifically
76
-            if 'class' in log:
77
-                l.klass = log['class']
78
-            if 'band' in log:
79
-                band = Band.query.filter(Band.name == log['band'],
80
-                                         Band.start.is_(None),
81
-                                         Band.modes.is_(None)).first()
82
+            if "class" in log:
83
+                l.klass = log["class"]
84
+            if "band" in log:
85
+                band = Band.query.filter(Band.name == log["band"], Band.start.is_(None), Band.modes.is_(None)).first()
82 86
                 if not band:
83
-                    band = Band.query.filter(Band.name == 'SSB',
84
-                                             Band.start.is_(None),
85
-                                             Band.modes.is_(None)).first()
87
+                    band = Band.query.filter(Band.name == "SSB", Band.start.is_(None), Band.modes.is_(None)).first()
86 88
                     if not l.notes:
87 89
                         l.notes = ""
88 90
                     l.notes += "\r\nBand automatically set to 40m because not found in ADIF"
89 91
                 l.band_id = band.id
90
-            if 'freq' in log:
91
-                l.freq = int(float(log['freq']) * 1000000)  # ADIF stores in MHz, we store in Hertz
92
-            if 'freq_rx' in log:
93
-                l.freq_rx = int(float(log['freq_rx']) * 1000000)  # Same as freq
94
-            if 'mode' in log:
95
-                mode = Mode.query.filter(Mode.mode == log['mode']).first()
92
+            if "freq" in log:
93
+                l.freq = int(float(log["freq"]) * 1000000)  # ADIF stores in MHz, we store in Hertz
94
+            if "freq_rx" in log:
95
+                l.freq_rx = int(float(log["freq_rx"]) * 1000000)  # Same as freq
96
+            if "mode" in log:
97
+                mode = Mode.query.filter(Mode.mode == log["mode"]).first()
96 98
                 if not mode:
97
-                    mode = Mode.query.filter(Mode.mode == 'SSB').first()
99
+                    mode = Mode.query.filter(Mode.mode == "SSB").first()
98 100
                     if not l.notes:
99 101
                         l.notes = ""
100 102
                     l.notes += "\r\nMode automatically set to SSB because not found in ADIF"
101 103
                 l.mode_id = mode.id
102 104
             # Reminder : ADIF is in UTC, we store in UTC, no TZ conversion necessary
103 105
             # TIME_ON
104
-            if 'qso_date' in log and 'time_on':
105
-                date = "{0} {1}".format(log['qso_date'], log['time_on'])
106
+            if "qso_date" in log and "time_on":
107
+                date = "{0} {1}".format(log["qso_date"], log["time_on"])
106 108
                 date_wo_tz = datetime.datetime.strptime(date, "%Y%m%d %H%M%S")
107 109
                 l.time_on = date_wo_tz
108 110
             else:
109 111
                 date_w_tz = datetime.datetime.utcnow()
110
-                l.time_on = date_w_tz.astimezone(pytz.timezone('UTC'))
112
+                l.time_on = date_w_tz.astimezone(pytz.timezone("UTC"))
111 113
                 if not l.notes:
112 114
                     l.notes = ""
113 115
                 l.notes = "\r\nDate and time_on set to the import date because not found in ADIF"
114 116
             # TIME_OFF
115
-            if 'qso_date' in log and 'time_off':
116
-                date = "{0} {1}".format(log['qso_date'], log['time_off'])
117
+            if "qso_date" in log and "time_off":
118
+                date = "{0} {1}".format(log["qso_date"], log["time_off"])
117 119
                 date_wo_tz = datetime.datetime.strptime(date, "%Y%m%d %H%M%S")
118 120
                 l.time_off = date_wo_tz
119 121
             else:
120 122
                 date_w_tz = datetime.datetime.utcnow()
121
-                l.time_off = date_w_tz.astimezone(pytz.timezone('UTC'))
123
+                l.time_off = date_w_tz.astimezone(pytz.timezone("UTC"))
122 124
                 if not l.notes:
123 125
                     l.notes = ""
124 126
                 l.notes = "\r\nDate and time_off set to the import date because not found in ADIF"
125 127
 
126
-            if 'comment' in log:
127
-                l.comment = log['comment']
128
-            if 'comment_intl' in log:
129
-                l.comment = log['comment_intl']
128
+            if "comment" in log:
129
+                l.comment = log["comment"]
130
+            if "comment_intl" in log:
131
+                l.comment = log["comment_intl"]
130 132
             l.user = current_user  # oops dont miss it
131 133
             l.logbook_id = _logbook.id
132 134
 
@@ -134,14 +136,14 @@ def adif_import_file():
134 136
             count += 1  # One more in the stack
135 137
         db.session.commit()
136 138
 
137
-        flash('Imported {0} ({1} duplicates) QSOs from {2}'.format(count, duplicates, filename), 'info')
139
+        flash("Imported {0} ({1} duplicates) QSOs from {2}".format(count, duplicates, filename), "info")
138 140
     else:
139
-        return render_template('tools/adif_import.jinja2', pcfg=pcfg, form=form, flash='Error with the file')
141
+        return render_template("tools/adif_import.jinja2", pcfg=pcfg, form=form, flash="Error with the file")
140 142
 
141
-    return redirect(url_for('bp_qsos.logbook', username=current_user.name, logbook_slug=_logbook.slug))
143
+    return redirect(url_for("bp_qsos.logbook", username=current_user.name, logbook_slug=_logbook.slug))
142 144
 
143 145
 
144
-@bp_tools.route('/user/<string:username>/logbook/<string:logbook_slug>/adif/export', methods=['GET'])
146
+@bp_tools.route("/user/<string:username>/logbook/<string:logbook_slug>/adif/export", methods=["GET"])
145 147
 @login_required
146 148
 def adif_export_dl(username, logbook_slug):
147 149
     """ http://hamclubs.info/adif-validator/ """
@@ -157,56 +159,56 @@ def adif_export_dl(username, logbook_slug):
157 159
 
158 160
     def a(k, v):
159 161
         v = str(v)
160
-        return u"<{0}:{1}>{2} ".format(k, len(v), v)
162
+        return "<{0}:{1}>{2} ".format(k, len(v), v)
161 163
 
162 164
     def ab(k, t, v):
163 165
         v = str(v)
164
-        return u"<{0}:{1}:{2}>{3} ".format(k, len(v), t, v)
166
+        return "<{0}:{1}:{2}>{3} ".format(k, len(v), t, v)
165 167
 
166 168
     def generate():
167
-        yield 'ADIF Export by AHRL\r\n'
168
-        yield '\r\n'
169
-        yield '<adif_ver:5>3.0.4\r\n'
170
-        yield '<programid:4>AHRL\r\n'
171
-        vers = '{0} git {1}'.format(g.cfg['AHRL_VERSION_VER'], g.cfg['AHRL_VERSION_GIT'])
172
-        yield a('programversion', vers) + '\r\n'
173
-        ct = datetime.datetime.utcnow().strftime('%Y%m%d %H%M%S')
174
-        yield a('created_timestamp', ct) + '\r\n'
175
-
176
-        yield ab('app_ahrl_station_callsign', 'S', user.callsign) + '\r\n'
177
-        yield ab('app_ahrl_operator', 'S', user.callsign) + '\r\n'
178
-        yield ab('app_ahrl_logbook', 'I', logbook.name) + '\r\n'
179
-        yield ab('app_ahrl_logbook_swl', 'B', 'Y' if logbook.swl else 'N') + '\r\n'
180
-        yield ab('app_ahrl_logbook_private', 'B', 'N' if logbook.public else 'Y') + '\r\n'
181
-
182
-        yield '\r\n'
183
-        yield '<eoh>\r\n\r\n'
169
+        yield "ADIF Export by AHRL\r\n"
170
+        yield "\r\n"
171
+        yield "<adif_ver:5>3.0.4\r\n"
172
+        yield "<programid:4>AHRL\r\n"
173
+        vers = "{0} git {1}".format(g.cfg["AHRL_VERSION_VER"], g.cfg["AHRL_VERSION_GIT"])
174
+        yield a("programversion", vers) + "\r\n"
175
+        ct = datetime.datetime.utcnow().strftime("%Y%m%d %H%M%S")
176
+        yield a("created_timestamp", ct) + "\r\n"
177
+
178
+        yield ab("app_ahrl_station_callsign", "S", user.callsign) + "\r\n"
179
+        yield ab("app_ahrl_operator", "S", user.callsign) + "\r\n"
180
+        yield ab("app_ahrl_logbook", "I", logbook.name) + "\r\n"
181
+        yield ab("app_ahrl_logbook_swl", "B", "Y" if logbook.swl else "N") + "\r\n"
182
+        yield ab("app_ahrl_logbook_private", "B", "N" if logbook.public else "Y") + "\r\n"
183
+
184
+        yield "\r\n"
185
+        yield "<eoh>\r\n\r\n"
184 186
 
185 187
         for log in logs:
186 188
             counter = 0
187 189
             for key in ADIF_FIELDS:
188 190
                 if counter == 4:
189 191
                     counter = 0
190
-                    yield '\r\n'
192
+                    yield "\r\n"
191 193
 
192 194
                 value = getattr(log, key)
193 195
                 if value:
194
-                    if key == 'swl' or key == 'force_init':
196
+                    if key == "swl" or key == "force_init":
195 197
                         if value == 1:
196
-                            val = 'Y'
198
+                            val = "Y"
197 199
                         elif value == 2:
198
-                            val = 'N'
200
+                            val = "N"
199 201
                         else:
200
-                            val = 'Y'
201
-                    elif key == 'lat':
202
-                        val = coordinates2adif(value, 'Latitude')
203
-                    elif key == 'lon':
204
-                        val = coordinates2adif(value, 'Longitude')
205
-                    elif key == 'notes':
206
-                        val = ''.join([i
207
-                                       if (126 >= ord(i) >= 32) or (i == '\r') or (i == '\n')
208
-                                       else ' ' for i in value])
209
-                    elif key == 'iota':
202
+                            val = "Y"
203
+                    elif key == "lat":
204
+                        val = coordinates2adif(value, "Latitude")
205
+                    elif key == "lon":
206
+                        val = coordinates2adif(value, "Longitude")
207
+                    elif key == "notes":
208
+                        val = "".join(
209
+                            [i if (126 >= ord(i) >= 32) or (i == "\r") or (i == "\n") else " " for i in value]
210
+                        )
211
+                    elif key == "iota":
210 212
                         if len(value) == 5:
211 213
                             val = "{0}-{1}".format(value[0:2], value[2:5])
212 214
                         else:
@@ -216,60 +218,69 @@ def adif_export_dl(username, logbook_slug):
216 218
                     yield a(key, val)
217 219
                     counter += 1
218 220
 
219
-            yield '\r\n'
221
+            yield "\r\n"
220 222
             # Manual ones
221 223
             if log.freq:
222
-                yield a('freq', log.freq / 1000000.0)
224
+                yield a("freq", log.freq / 1000000.0)
223 225
             if log.freq_rx:
224
-                yield a('freq', log.freq_rx / 1000000.0)
226
+                yield a("freq", log.freq_rx / 1000000.0)
225 227
             if log.mode:
226
-                yield a('mode', log.mode.mode)
228
+                yield a("mode", log.mode.mode)
227 229
             if log.time_on:
228
-                yield a('qso_date', log.time_on.strftime('%Y%m%d'))
229
-                yield a('time_on', log.time_on.strftime('%H%M%S'))
230
+                yield a("qso_date", log.time_on.strftime("%Y%m%d"))
231
+                yield a("time_on", log.time_on.strftime("%H%M%S"))
230 232
             if log.time_off:
231
-                yield a('time_off', log.time_off.strftime('%H%M%S'))
233
+                yield a("time_off", log.time_off.strftime("%H%M%S"))
232 234
             if log.klass:
233
-                yield a('class', log.klass)
235
+                yield a("class", log.klass)
234 236
             if log.band:
235
-                yield a('band', log.band.name)
237
+                yield a("band", log.band.name)
236 238
             if log.comment:
237
-                yield a('comment_intl', log.comment)
238
-            yield '\r\n<eor>\r\n\r\n'
239
+                yield a("comment_intl", log.comment)
240
+            yield "\r\n<eor>\r\n\r\n"
239 241
 
240
-    return Response(stream_with_context(generate()), mimetype="text/plain",
241
-                    headers={"Content-Disposition": "attachment;filename=qsos-{0}.adi".format(user.name)})
242
+    return Response(
243
+        stream_with_context(generate()),
244
+        mimetype="text/plain",
245
+        headers={"Content-Disposition": "attachment;filename=qsos-{0}.adi".format(user.name)},
246
+    )
242 247
 
243 248
 
244
-@bp_tools.route('/tools/bands/plan', methods=['GET'])
249
+@bp_tools.route("/tools/bands/plan", methods=["GET"])
245 250
 def bands_plan():
246 251
     pcfg = {"title": "IARU Band Plans"}
247 252
 
248 253
     bands = {
249
-        'iaru1': {
250
-            'name': 'IARU Zone 1',
251
-            'slug': 'iaru1',
252
-            'bands': db.session.query(Band.lower, Band.upper, Band.name).filter(
253
-                Band.modes.is_(None), Band.start.is_(None), Band.zone == 'iaru1').order_by(Band.lower.asc()).all()
254
+        "iaru1": {
255
+            "name": "IARU Zone 1",
256
+            "slug": "iaru1",
257
+            "bands": db.session.query(Band.lower, Band.upper, Band.name)
258
+            .filter(Band.modes.is_(None), Band.start.is_(None), Band.zone == "iaru1")
259
+            .order_by(Band.lower.asc())
260
+            .all(),
254 261
         },
255
-        'iaru2': {
256
-            'name': 'IARU Zone 2',
257
-            'slug': 'iaru2',
258
-            'bands': db.session.query(Band.lower, Band.upper, Band.name).filter(
259
-                Band.modes.is_(None), Band.start.is_(None), Band.zone == 'iaru2').order_by(Band.lower.asc()).all()
262
+        "iaru2": {
263
+            "name": "IARU Zone 2",
264
+            "slug": "iaru2",
265
+            "bands": db.session.query(Band.lower, Band.upper, Band.name)
266
+            .filter(Band.modes.is_(None), Band.start.is_(None), Band.zone == "iaru2")
267
+            .order_by(Band.lower.asc())
268
+            .all(),
269
+        },
270
+        "iaru3": {
271
+            "name": "IARU Zone 3",
272
+            "slug": "iaru3",
273
+            "bands": db.session.query(Band.lower, Band.upper, Band.name)
274
+            .filter(Band.modes.is_(None), Band.start.is_(None), Band.zone == "iaru3")
275
+            .order_by(Band.lower.asc())
276
+            .all(),
260 277
         },
261
-        'iaru3': {
262
-            'name': 'IARU Zone 3',
263
-            'slug': 'iaru3',
264
-            'bands': db.session.query(Band.lower, Band.upper, Band.name).filter(
265
-                Band.modes.is_(None), Band.start.is_(None), Band.zone == 'iaru3').order_by(Band.lower.asc()).all()
266
-        }
267 278
     }
268
-    return render_template('tools/bands_plan.jinja2', pcfg=pcfg, bands=bands)
279
+    return render_template("tools/bands_plan.jinja2", pcfg=pcfg, bands=bands)
269 280
 
270 281
 
271
-@bp_tools.route('/tools/map', methods=['GET'])
282
+@bp_tools.route("/tools/map", methods=["GET"])
272 283
 @check_default_profile
273 284
 def map():
274 285
     pcfg = {"title": "It's a map"}
275
-    return render_template('tools/map.jinja2', pcfg=pcfg)
286
+    return render_template("tools/map.jinja2", pcfg=pcfg)

+ 38
- 21
controllers/users.py View File

@@ -7,26 +7,33 @@ from forms import UserProfileForm
7 7
 from models import db, User, Logbook, Log, UserLogging
8 8
 from utils import check_default_profile
9 9
 
10
-bp_users = Blueprint('bp_users', __name__)
10
+bp_users = Blueprint("bp_users", __name__)
11 11
 
12 12
 
13
-@bp_users.route('/user/logs', methods=['GET'])
13
+@bp_users.route("/user/logs", methods=["GET"])
14 14
 @login_required
15 15
 @check_default_profile
16 16
 def logs():
17
-    level = request.args.get('level')
17
+    level = request.args.get("level")
18 18
     pcfg = {"title": "User Logs"}
19 19
     if level:
20
-        _logs = UserLogging.query.filter(UserLogging.level == level.upper(),
21
-                                         UserLogging.user_id == current_user.id
22
-                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
20
+        _logs = (
21
+            UserLogging.query.filter(UserLogging.level == level.upper(), UserLogging.user_id == current_user.id)
22
+            .order_by(UserLogging.timestamp.desc())
23
+            .limit(100)
24
+            .all()
25
+        )
23 26
     else:
24
-        _logs = UserLogging.query.filter(UserLogging.user_id == current_user.id
25
-                                         ).order_by(UserLogging.timestamp.desc()).limit(100).all()
26
-    return render_template('users/user_logs.jinja2', pcfg=pcfg, logs=_logs)
27
+        _logs = (
28
+            UserLogging.query.filter(UserLogging.user_id == current_user.id)
29
+            .order_by(UserLogging.timestamp.desc())
30
+            .limit(100)
31
+            .all()
32
+        )
33
+    return render_template("users/user_logs.jinja2", pcfg=pcfg, logs=_logs)
27 34
 
28 35
 
29
-@bp_users.route('/user', methods=['GET'])
36
+@bp_users.route("/user", methods=["GET"])
30 37
 @login_required
31 38
 @check_default_profile
32 39
 def profile():
@@ -34,22 +41,27 @@ def profile():
34 41
 
35 42
     user = User.query.filter(User.id == current_user.id).first()
36 43
     if not user:
37
-        flash("User not found", 'error')
44
+        flash("User not found", "error")
38 45
         return redirect(url_for("bp_main.home"))
39 46
 
40
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
41
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
42
-    return render_template('users/profile.jinja2', pcfg=pcfg, user=user, logbooks=logbooks)
47
+    logbooks = (
48
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
49
+        .join(Log)
50
+        .filter(Logbook.user_id == current_user.id)
51
+        .group_by(Logbook.id)
52
+        .all()
53
+    )
54
+    return render_template("users/profile.jinja2", pcfg=pcfg, user=user, logbooks=logbooks)
43 55
 
44 56
 
45
-@bp_users.route('/user/edit', methods=['GET', 'POST'])
57
+@bp_users.route("/user/edit", methods=["GET", "POST"])
46 58
 @login_required
47 59
 def edit():
48 60
     pcfg = {"title": "Edit my profile"}
49 61
 
50 62
     user = User.query.filter(User.id == current_user.id).first()
51 63
     if not user:
52
-        flash("User not found", 'error')
64
+        flash("User not found", "error")
53 65
         return redirect(url_for("bp_main.home"))
54 66
 
55 67
     form = UserProfileForm(request.form, obj=user)
@@ -75,8 +87,13 @@ def edit():
75 87
         user.zone = form.zone.data
76 88
 
77 89
         db.session.commit()
78
-        return redirect(url_for('bp_users.profile'))
79
-
80
-    logbooks = db.session.query(Logbook.id, Logbook.name, func.count(Log.id)).join(
81
-        Log).filter(Logbook.user_id == current_user.id).group_by(Logbook.id).all()
82
-    return render_template('users/edit.jinja2', pcfg=pcfg, form=form, user=user, logbooks=logbooks)
90
+        return redirect(url_for("bp_users.profile"))
91
+
92
+    logbooks = (
93
+        db.session.query(Logbook.id, Logbook.name, func.count(Log.id))
94
+        .join(Log)
95
+        .filter(Logbook.user_id == current_user.id)
96
+        .group_by(Logbook.id)
97
+        .all()
98
+    )
99
+    return render_template("users/edit.jinja2", pcfg=pcfg, form=form, user=user, logbooks=logbooks)

+ 106
- 103
crons.py View File

@@ -41,30 +41,30 @@ def update_qsos_from_hamqth():
41 41
                 err.user_id = log.user.id
42 42
                 err.log_id = log.id
43 43
                 err.logbook_id = log.logbook.id
44
-                err.category = 'HamQTH'
45
-                err.level = 'ERROR'
46
-                err.message = 'Query failed or call not found: {0}'.format(e)
44
+                err.category = "HamQTH"
45
+                err.level = "ERROR"
46
+                err.message = "Query failed or call not found: {0}".format(e)
47 47
                 db.session.add(err)
48 48
                 log.consolidated_hamqth = True
49 49
                 continue
50 50
 
51
-            if 'nick' in _csd and not log.name:
52
-                log.name = _csd['nick']
53
-            if 'qth' in _csd and not log.qth:
54
-                log.qth = _csd['qth']
55
-            if 'grid' in _csd and not log.gridsquare:
56
-                log.gridsquare = _csd['grid']
51
+            if "nick" in _csd and not log.name:
52
+                log.name = _csd["nick"]
53
+            if "qth" in _csd and not log.qth:
54
+                log.qth = _csd["qth"]
55
+            if "grid" in _csd and not log.gridsquare:
56
+                log.gridsquare = _csd["grid"]
57 57
             # if 'country' in _csd and not log.country:
58 58
             #    log.country = _csd['country']
59 59
             # We must leave country filled by DXCC Clublog or Database I think finally
60
-            if 'latitude' in _csd and not log.lat:
61
-                log.lat = _csd['latitude']
62
-            if 'longitude' in _csd and not log.lon:
63
-                log.lon = _csd['longitude']
64
-            if 'web' in _csd and not log.web:
65
-                log.web = _csd['web']
66
-            if 'iota' in _csd and not log.iota:
67
-                log.iota = _csd['iota']
60
+            if "latitude" in _csd and not log.lat:
61
+                log.lat = _csd["latitude"]
62
+            if "longitude" in _csd and not log.lon:
63
+                log.lon = _csd["longitude"]
64
+            if "web" in _csd and not log.web:
65
+                log.web = _csd["web"]
66
+            if "iota" in _csd and not log.iota:
67
+                log.iota = _csd["iota"]
68 68
 
69 69
             log.consolidated_hamqth = True
70 70
             updated += 1
@@ -81,10 +81,10 @@ def update_qsos_without_countries():
81 81
         if not log.call:
82 82
             continue
83 83
         dxcc = get_dxcc_from_clublog_or_database(log.call)
84
-        log.dxcc = dxcc['DXCC']
85
-        log.cqz = dxcc['CQZ']
86
-        log.country = dxcc['Name']
87
-        log.cont = dxcc['Continent']
84
+        log.dxcc = dxcc["DXCC"]
85
+        log.cqz = dxcc["CQZ"]
86
+        log.country = dxcc["Name"]
87
+        log.cont = dxcc["Continent"]
88 88
         db.session.commit()
89 89
         updated += 1
90 90
     print("Updated {0} QSOs".format(updated))
@@ -98,7 +98,7 @@ def populate_logs_gridsquare_cache():
98 98
         if not qth:
99 99
             print("!! country_grid_coords() for log {0} returned None, please check !!".format(log.id))
100 100
             continue
101
-        log.cache_gridsquare = qth['qth']
101
+        log.cache_gridsquare = qth["qth"]
102 102
         updated += 1
103 103
     db.session.commit()
104 104
     print("-- Updated {0} QSOs".format(updated))
@@ -107,13 +107,13 @@ def populate_logs_gridsquare_cache():
107 107
 def update_dxcc_from_cty_xml(_file=None, silent=False):
108 108
     if not silent:
109 109
         print("--- Updating DXCC tables (prefixes, entities, exceptions) from cty.xml")
110
-    fname = os.path.join(current_app.config['TEMP_DOWNLOAD_FOLDER'], 'cty.xml')
110
+    fname = os.path.join(current_app.config["TEMP_DOWNLOAD_FOLDER"], "cty.xml")
111 111
 
112 112
     config = Config.query.first()
113 113
     if not config:
114 114
         if not silent:
115 115
             print("!!! Error: config not found")
116
-        add_log(category='CONFIG', level='ERROR', message='Config not found')
116
+        add_log(category="CONFIG", level="ERROR", message="Config not found")
117 117
         return
118 118
 
119 119
     if os.path.isfile(fname):
@@ -127,12 +127,12 @@ def update_dxcc_from_cty_xml(_file=None, silent=False):
127 127
         if not config.clublog_api_key:
128 128
             if not silent:
129 129
                 print("!! Clublog API Key not defined")
130
-            add_log(category='CRONS', level='ERROR', message='Clublog API Key not defined')
130
+            add_log(category="CRONS", level="ERROR", message="Clublog API Key not defined")
131 131
             return
132 132
         url = "https://secure.clublog.org/cty.php?api={0}".format(config.clublog_api_key)
133 133
 
134 134
         try:
135
-            with urllib.request.urlopen(url) as response, open(fname, 'wb') as out_file:
135
+            with urllib.request.urlopen(url) as response, open(fname, "wb") as out_file:
136 136
                 with gzip.GzipFile(fileobj=response) as uncompressed:
137 137
                     shutil.copyfileobj(uncompressed, out_file)
138 138
         except urllib.error.URLError as err:
@@ -169,28 +169,28 @@ def update_dxcc_from_cty_xml(_file=None, silent=False):
169 169
     root = tree.getroot()
170 170
 
171 171
     for element in root:
172
-        if element.tag == '{http://www.clublog.org/cty/v1.0}entities':
172
+        if element.tag == "{http://www.clublog.org/cty/v1.0}entities":
173 173
             if not silent:
174
-                print('++ Parsing {0}'.format(element.tag))
174
+                print("++ Parsing {0}".format(element.tag))
175 175
             rmed = DxccEntities.query.delete()
176 176
             if not silent:
177
-                print('-- Cleaned {0} old entries'.format(rmed))
177
+                print("-- Cleaned {0} old entries".format(rmed))
178 178
             parse_element(element, silent)
179 179
 
180
-        elif element.tag == '{http://www.clublog.org/cty/v1.0}exceptions':
180
+        elif element.tag == "{http://www.clublog.org/cty/v1.0}exceptions":
181 181
             if not silent:
182
-                print('++ Parsing {0}'.format(element.tag))
182
+                print("++ Parsing {0}".format(element.tag))
183 183
             rmed = DxccExceptions.query.delete()
184 184
             if not silent:
185
-                print('-- Cleaned {0} old entries'.format(rmed))
185
+                print("-- Cleaned {0} old entries".format(rmed))
186 186
             parse_element(element, silent)
187 187
 
188
-        elif element.tag == '{http://www.clublog.org/cty/v1.0}prefixes':
188
+        elif element.tag == "{http://www.clublog.org/cty/v1.0}prefixes":
189 189
             if not silent:
190
-                print('++ Parsing {0}'.format(element.tag))
190
+                print("++ Parsing {0}".format(element.tag))
191 191
             rmed = DxccPrefixes.query.delete()
192 192
             if not silent:
193
-                print('-- Cleaned {0} old entries'.format(rmed))
193
+                print("-- Cleaned {0} old entries".format(rmed))
194 194
             parse_element(element, silent)
195 195
 
196 196
 
@@ -199,43 +199,43 @@ def parse_element(element, silent=False):
199 199
     for child in element:
200 200
         skip = False
201 201
 
202
-        if element.tag == '{http://www.clublog.org/cty/v1.0}entities':
202
+        if element.tag == "{http://www.clublog.org/cty/v1.0}entities":
203 203
             _obj = DxccEntities()
204 204
             _obj.ituz = 999  # We don't have ITUZ in cty.xml so we put 999 in it
205
-        elif element.tag == '{http://www.clublog.org/cty/v1.0}exceptions':
205
+        elif element.tag == "{http://www.clublog.org/cty/v1.0}exceptions":
206 206
             _obj = DxccExceptions()
207
-        elif element.tag == '{http://www.clublog.org/cty/v1.0}prefixes':
207
+        elif element.tag == "{http://www.clublog.org/cty/v1.0}prefixes":
208 208
             _obj = DxccPrefixes()
209 209
         else:
210 210
             return
211 211
 
212
-        if 'record' in child.attrib:
213
-            _obj.record = child.attrib['record']
212
+        if "record" in child.attrib:
213
+            _obj.record = child.attrib["record"]
214 214
 
215 215
         for attr in child:
216
-            if attr.tag == '{http://www.clublog.org/cty/v1.0}call':
216
+            if attr.tag == "{http://www.clublog.org/cty/v1.0}call":
217 217
                 _obj.call = attr.text
218
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}name':
218
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}name":
219 219
                 _obj.name = attr.text
220
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}prefix':
220
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}prefix":
221 221
                 _obj.prefix = attr.text
222
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}entity':
223
-                if attr.text == 'INVALID':
222
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}entity":
223
+                if attr.text == "INVALID":
224 224
                     skip = True
225 225
                 _obj.entity = attr.text
226
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}adif':
226
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}adif":
227 227
                 _obj.adif = int(attr.text)
228
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}cqz':
228
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}cqz":
229 229
                 _obj.cqz = float(attr.text)
230
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}cont':
230
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}cont":
231 231
                 _obj.cont = attr.text
232
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}long':
232
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}long":
233 233
                 _obj.long = float(attr.text)
234
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}lat':
234
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}lat":
235 235
                 _obj.lat = float(attr.text)
236
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}start':
236
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}start":
237 237
                 _obj.start = parser.parse(attr.text)
238
-            elif attr.tag == '{http://www.clublog.org/cty/v1.0}end':
238
+            elif attr.tag == "{http://www.clublog.org/cty/v1.0}end":
239 239
                 _obj.start = parser.parse(attr.text)
240 240
 
241 241
             if not _obj.adif:
@@ -250,7 +250,7 @@ def parse_element(element, silent=False):
250 250
         elements += 1
251 251
     db.session.commit()
252 252
     if not silent:
253
-        print('-- Committed {0} new elements'.format(elements))
253
+        print("-- Committed {0} new elements".format(elements))
254 254
 
255 255
 
256 256
 def cron_sync_from_eqsl(dry_run=False):
@@ -274,16 +274,18 @@ def cron_sync_from_eqsl(dry_run=False):
274 274
         config = Config.query.first()
275 275
         if not config:
276 276
             print("!!! Error: config not found")
277
-            add_log(category='CONFIG', level='ERROR', message='Config not found')
277
+            add_log(category="CONFIG", level="ERROR", message="Config not found")
278 278
             return
279 279
 
280 280
         print("-- Working on logbook [{0}] {1}".format(logbook.id, logbook.name))
281 281
 
282
-        _payload = urllib.parse.urlencode({
283
-            "UserName": logbook.user.eqsl_name,
284
-            "Password": logbook.user.eqsl_password,
285
-            "QTHNickname": logbook.eqsl_qth_nickname
286
-        })
282
+        _payload = urllib.parse.urlencode(
283
+            {
284
+                "UserName": logbook.user.eqsl_name,
285
+                "Password": logbook.user.eqsl_password,
286
+                "QTHNickname": logbook.eqsl_qth_nickname,
287
+            }
288
+        )
287 289
 
288 290
         _url = "{0}?{1}".format(config.eqsl_download_url, _payload)
289 291
 
@@ -293,21 +295,21 @@ def cron_sync_from_eqsl(dry_run=False):
293 295
         err_fetch = UserLogging()
294 296
         err_fetch.user_id = logbook.user.id
295 297
         err_fetch.logbook_id = logbook.id
296
-        err_fetch.category = 'EQSL FETCH'
298
+        err_fetch.category = "EQSL FETCH"
297 299
 
298 300
         try:
299 301
             with urllib.request.urlopen(_req) as f:
300
-                _text = f.read().decode('UTF-8')
302
+                _text = f.read().decode("UTF-8")
301 303
         except urllib.error.URLError as e:
302
-            err_fetch.level = 'ERROR'
303
-            err_fetch.message = 'Error fetching from eQSL: {0}'.format(e)
304
+            err_fetch.level = "ERROR"
305
+            err_fetch.message = "Error fetching from eQSL: {0}".format(e)
304 306
             db.session.add(err_fetch)
305 307
             db.session.commit()
306 308
             continue  # skip to next
307 309
 
308 310
         if not _text:
309
-            err_fetch.level = 'ERROR'
310
-            err_fetch.message = 'Error fetching from EQSL, _text undefined'
311
+            err_fetch.level = "ERROR"
312
+            err_fetch.message = "Error fetching from EQSL, _text undefined"
311 313
             db.session.add(err_fetch)
312 314
             db.session.commit()
313 315
             continue  # skip to next
@@ -326,17 +328,17 @@ def cron_sync_from_eqsl(dry_run=False):
326 328
                 print("-- Fetching ADIF {0}".format(_url))
327 329
                 with urllib.request.urlopen(_req) as f:
328 330
                     # eQSL returns a file encoded in ISO8859-1 so decode it then re-encode it in UTF-8
329
-                    _text = f.read().decode('ISO8859-1').encode('UTF-8')
331
+                    _text = f.read().decode("ISO8859-1").encode("UTF-8")
330 332
             except urllib.error.URLError as e:
331
-                err_fetch.level = 'ERROR'
332
-                err_fetch.message = 'Error fetching from eQSL: {0}'.format(e)
333
+                err_fetch.level = "ERROR"
334
+                err_fetch.message = "Error fetching from eQSL: {0}".format(e)
333 335
                 db.session.add(err_fetch)
334 336
                 db.session.commit()
335 337
                 continue  # skip to next
336 338
 
337 339
             if not _text:
338
-                err_fetch.level = 'ERROR'
339
-                err_fetch.message = 'Error fetching from EQSL, _text for final URL undefined'
340
+                err_fetch.level = "ERROR"
341
+                err_fetch.message = "Error fetching from EQSL, _text for final URL undefined"
340 342
                 db.session.add(err_fetch)
341 343
                 db.session.commit()
342 344
                 continue  # skip to next
@@ -347,36 +349,37 @@ def cron_sync_from_eqsl(dry_run=False):
347 349
                 err_log = UserLogging()
348 350
                 err_log.user_id = logbook.user.id
349 351
                 err_log.logbook_id = logbook.id
350
-                err_log.category = 'EQSL LOG'
352
+                err_log.category = "EQSL LOG"
351 353
 
352 354
                 _date = "{0} {1}".format(log["qso_date"], log["time_on"])
353 355
                 _date_first = datetime.datetime.strptime(_date + "00", "%Y%m%d %H%M%S")
354 356
                 _date_second = datetime.datetime.strptime(_date + "59", "%Y%m%d %H%M%S")
355 357
                 # Try to find a matching log entry
356
-                qso = Log.query.filter(Log.logbook_id == logbook.id,
357
-                                       Log.user_id == logbook.user.id,
358
-                                       Log.call == log['call'].upper(),
359
-                                       Log.time_on.between(_date_first, _date_second)).first()
358
+                qso = Log.query.filter(
359
+                    Log.logbook_id == logbook.id,
360
+                    Log.user_id == logbook.user.id,
361
+                    Log.call == log["call"].upper(),
362
+                    Log.time_on.between(_date_first, _date_second),
363
+                ).first()
360 364
                 if qso:
361
-                    if qso.eqsl_qsl_rcvd == 'Y':
365
+                    if qso.eqsl_qsl_rcvd == "Y":
362 366
                         continue  # this eQSL have already been matched
363
-                    print("-- Matching log found for {0} on {1} : ID {2}".format(log['call'],
364
-                                                                                 _date, qso.id))
367
+                    print("-- Matching log found for {0} on {1} : ID {2}".format(log["call"], _date, qso.id))
365 368
                     if not dry_run:
366
-                        qso.eqsl_qsl_rcvd = 'Y'
369
+                        qso.eqsl_qsl_rcvd = "Y"
367 370
                         err_log.log_id = qso.id
368
-                        err_log.level = 'INFO'
369
-                        err_log.message = 'QSO from eQSL by {0} on {1} received and updated'.format(log['call'], _date)
371
+                        err_log.level = "INFO"
372
+                        err_log.message = "QSO from eQSL by {0} on {1} received and updated".format(log["call"], _date)
370 373
                 else:
371
-                    print("-- No matching log found for {0} on {1}".format(log['call'], _date))
372
-                    err_log.level = 'INFO'
373
-                    err_log.message = 'QSO from eQSL by {0} on {1} not found in database'.format(log['call'], _date)
374
+                    print("-- No matching log found for {0} on {1}".format(log["call"], _date))
375
+                    err_log.level = "INFO"
376
+                    err_log.message = "QSO from eQSL by {0} on {1} not found in database".format(log["call"], _date)
374 377
                 if not dry_run:
375 378
                     db.session.add(err_log)
376 379
                     db.session.commit()
377 380
         else:
378
-            err_fetch.level = 'ERROR'
379
-            err_fetch.message = 'Error fetching from EQSL, link not found in body'
381
+            err_fetch.level = "ERROR"
382
+            err_fetch.message = "Error fetching from EQSL, link not found in body"
380 383
             db.session.add(err_fetch)
381 384
             db.session.commit()
382 385
 
@@ -388,11 +391,11 @@ def cron_sync_eqsl(dry_run=False):
388 391
         print("--- [DRY RUN] Sending logs to eQSL when requested")
389 392
     else:
390 393
         print("--- Sending logs to eQSL when requested")
391
-    logs = Log.query.filter(Log.eqsl_qsl_sent == 'R').all()
394
+    logs = Log.query.filter(Log.eqsl_qsl_sent == "R").all()
392 395
     config = Config.query.first()
393 396
     if not config:
394 397
         print("!!! Error: config not found")
395
-        add_log(category='CONFIG', level='ERROR', message='Config not found')
398
+        add_log(category="CONFIG", level="ERROR", message="Config not found")
396 399
         return
397 400
 
398 401
     for log in logs:
@@ -403,26 +406,26 @@ def cron_sync_eqsl(dry_run=False):
403 406
         err.user_id = log.user.id
404 407
         err.log_id = log.id
405 408
         err.logbook_id = log.logbook.id
406
-        err.category = 'EQSL'
407
-
408
-        if status['state'] == 'error':
409
-            err.level = 'ERROR'
410
-            print("!! Error uploading QSO {0} to eQSL: {1}".format(log.id, status['message']))
411
-        elif status['state'] == 'rejected':
412
-            log.eqsl_qsl_sent = 'I'
413
-            print("!! Rejected uploading QSO {0} to eQSL: {1}".format(log.id, status['message']))
409
+        err.category = "EQSL"
410
+
411
+        if status["state"] == "error":
412
+            err.level = "ERROR"
413
+            print("!! Error uploading QSO {0} to eQSL: {1}".format(log.id, status["message"]))
414
+        elif status["state"] == "rejected":
415
+            log.eqsl_qsl_sent = "I"
416
+            print("!! Rejected uploading QSO {0} to eQSL: {1}".format(log.id, status["message"]))
414 417
         else:
415
-            err.level = 'INFO'
418
+            err.level = "INFO"
416 419
 
417
-        err.message = status['message'] + '\r\n'
420
+        err.message = status["message"] + "\r\n"
418 421
 
419
-        if 'msgs' in status:
420
-            for i in status['msgs']:
422
+        if "msgs" in status:
423
+            for i in status["msgs"]:
421 424
                 print("!! {0}: {1}".format(i[0], i[1]))
422
-                err.message += '{0}: {1}\r\n'.format(i[0], i[1])
425
+                err.message += "{0}: {1}\r\n".format(i[0], i[1])
423 426
 
424
-        if status['state'] == 'success':
425
-            log.eqsl_qsl_sent = 'Y'
427
+        if status["state"] == "success":
428
+            log.eqsl_qsl_sent = "Y"
426 429
 
427 430
         print(status)
428 431
 

+ 137
- 134
dbseed.py View File

@@ -17,163 +17,166 @@ def make_db_seed(db):
17 17
 def seed_users(db):
18 18
     print("++ Seeding users")
19 19
     role_usr = Role()
20
-    role_usr.name = 'user'
21
-    role_usr.description = 'Simple user'
20
+    role_usr.name = "user"
21
+    role_usr.description = "Simple user"
22 22
 
23 23
     role_adm = Role()
24
-    role_adm.name = 'admin'
25
-    role_adm.description = 'Admin user'
24
+    role_adm.name = "admin"
25
+    role_adm.description = "Admin user"
26 26
 
27 27
     db.session.add(role_usr)
28 28
     db.session.add(role_adm)
29 29
 
30 30
     user_datastore.create_user(
31
-        email='dashie@sigpipe.me',
32
-        password='fluttershy',
33
-        name='toto',
34
-        timezone='UTC',
31
+        email="dashie@sigpipe.me",
32
+        password="fluttershy",
33
+        name="toto",
34
+        timezone="UTC",
35 35
         roles=[role_adm],
36
-        callsign='N0C4LL',
37
-        locator='JN'
36
+        callsign="N0C4LL",
37
+        locator="JN",
38 38
     )
39 39
     db.session.commit()
40 40
     return
41 41
 
42 42
 
43 43
 def seed_config(db):
44
-    a = Config(lotw_download_url='https://p1k.arrl.org/lotwuser/lotwreport.adi',
45
-               lotw_upload_url='https://p1k.arrl.org/lotwuser/upload',
46
-               lotw_rcvd_mark='Y',
47
-               lotw_login_url='https://p1k.arrl.org/lotwuser/default',
48
-               eqsl_download_url='https://www.eqsl.cc/qslcard/DownloadInBox.cfm',
49
-               eqsl_upload_url='https://www.eqsl.cc/qslcard/ImportADIF.cfm',
50
-               eqsl_rcvd_mark='Y')
44
+    a = Config(
45
+        lotw_download_url="https://p1k.arrl.org/lotwuser/lotwreport.adi",
46
+        lotw_upload_url="https://p1k.arrl.org/lotwuser/upload",
47
+        lotw_rcvd_mark="Y",
48
+        lotw_login_url="https://p1k.arrl.org/lotwuser/default",
49
+        eqsl_download_url="https://www.eqsl.cc/qslcard/DownloadInBox.cfm",
50
+        eqsl_upload_url="https://www.eqsl.cc/qslcard/ImportADIF.cfm",
51
+        eqsl_rcvd_mark="Y",
52
+    )
51 53
     db.session.add(a)
52 54
     db.session.commit()
53 55
     db.session.commit()
54 56
     # Bug, two commit necessary
55 57
 
58
+
56 59
 #### Only used by tests
57 60
 def seed_bands(db):
58
-    db.session.add(Band(name='2222m', zone='iaru1', lower=135700, upper=137800))
59
-    db.session.add(Band(name='630m', zone='iaru1', lower=472000, upper=476000))
60
-    db.session.add(Band(name='160m', zone='iaru1', lower=1810000, upper=1850000))
61
-    db.session.add(Band(name='80m', zone='iaru1', lower=3500000, upper=3800000))
62
-    db.session.add(Band(name='60m', zone='iaru1', lower=5351500, upper=5366500))
63
-    db.session.add(Band(name='40m', zone='iaru1', lower=7000000, upper=7200000))
64
-    db.session.add(Band(name='30m', zone='iaru1', lower=10100000, upper=10150000))
65
-    db.session.add(Band(name='20m', zone='iaru1', lower=14000000, upper=14350000))
66
-    db.session.add(Band(name='17m', zone='iaru1', lower=18068000, upper=18168000))
67
-    db.session.add(Band(name='15m', zone='iaru1', lower=21000000, upper=21450000))
68
-    db.session.add(Band(name='12m', zone='iaru1', lower=24890000, upper=24990000))
69
-    db.session.add(Band(name='10m', zone='iaru1', lower=28000000, upper=29700000))
70
-    db.session.add(Band(name='6m', zone='iaru1', lower=50000000, upper=52000000))
71
-    db.session.add(Band(name='2m', zone='iaru1', lower=144000000, upper=146000000))
72
-    db.session.add(Band(name='70cm', zone='iaru1', lower=430000000, upper=440000000))
73
-    db.session.add(Band(name='23cm', zone='iaru1', lower=1240000000, upper=1300000000))
74
-    db.session.add(Band(name='13cm', zone='iaru1', lower=2300000000, upper=2450000000))
75
-    db.session.add(Band(name='5cm', zone='iaru1', lower=5650000000, upper=5850000000))
76
-    db.session.add(Band(name='3cm', zone='iaru1', lower=10000000000, upper=10500000000))
77
-    db.session.add(Band(name='1,2cm', zone='iaru1', lower=24000000000, upper=24250000000))
78
-    db.session.add(Band(name='6mm', zone='iaru1', lower=47000000000, upper=47200000000))
79
-    db.session.add(Band(name='4mm', zone='iaru1', lower=76000000000, upper=81500000000))
80
-    db.session.add(Band(name='2,4mm', zone='iaru1', lower=122250000000, upper=123000000000))
81
-    db.session.add(Band(name='2mm', zone='iaru1', lower=134000000000, upper=141000000000))
82
-    db.session.add(Band(name='1,2mm', zone='iaru1', lower=241000000000, upper=250000000000))
61
+    db.session.add(Band(name="2222m", zone="iaru1", lower=135700, upper=137800))
62
+    db.session.add(Band(name="630m", zone="iaru1", lower=472000, upper=476000))
63
+    db.session.add(Band(name="160m", zone="iaru1", lower=1810000, upper=1850000))
64
+    db.session.add(Band(name="80m", zone="iaru1", lower=3500000, upper=3800000))
65
+    db.session.add(Band(name="60m", zone="iaru1", lower=5351500, upper=5366500))
66
+    db.session.add(Band(name="40m", zone="iaru1", lower=7000000, upper=7200000))
67
+    db.session.add(Band(name="30m", zone="iaru1", lower=10100000, upper=10150000))
68
+    db.session.add(Band(name="20m", zone="iaru1", lower=14000000, upper=14350000))
69
+    db.session.add(Band(name="17m", zone="iaru1", lower=18068000, upper=18168000))
70
+    db.session.add(Band(name="15m", zone="iaru1", lower=21000000, upper=21450000))
71
+    db.session.add(Band(name="12m", zone="iaru1", lower=24890000, upper=24990000))
72
+    db.session.add(Band(name="10m", zone="iaru1", lower=28000000, upper=29700000))
73
+    db.session.add(Band(name="6m", zone="iaru1", lower=50000000, upper=52000000))
74
+    db.session.add(Band(name="2m", zone="iaru1", lower=144000000, upper=146000000))
75
+    db.session.add(Band(name="70cm", zone="iaru1", lower=430000000, upper=440000000))
76
+    db.session.add(Band(name="23cm", zone="iaru1", lower=1240000000, upper=1300000000))
77
+    db.session.add(Band(name="13cm", zone="iaru1", lower=2300000000, upper=2450000000))
78
+    db.session.add(Band(name="5cm", zone="iaru1", lower=5650000000, upper=5850000000))
79
+    db.session.add(Band(name="3cm", zone="iaru1", lower=10000000000, upper=10500000000))
80
+    db.session.add(Band(name="1,2cm", zone="iaru1", lower=24000000000, upper=24250000000))
81
+    db.session.add(Band(name="6mm", zone="iaru1", lower=47000000000, upper=47200000000))
82
+    db.session.add(Band(name="4mm", zone="iaru1", lower=76000000000, upper=81500000000))
83
+    db.session.add(Band(name="2,4mm", zone="iaru1", lower=122250000000, upper=123000000000))
84
+    db.session.add(Band(name="2mm", zone="iaru1", lower=134000000000, upper=141000000000))
85
+    db.session.add(Band(name="1,2mm", zone="iaru1", lower=241000000000, upper=250000000000))
83 86
 
84 87
     # Modes plans for IARU1
85 88
     # Only CW on 2222m
86
-    db.session.add(Band(name='2222m', zone='iaru1', modes=None, start=135700))
87
-
88
-    db.session.add(Band(name='160m', zone='iaru1', modes="CW", start=1810000))
89
-    db.session.add(Band(name='160m', zone='iaru1', modes="RTTY,PSK31,PSK63,SSTV", start=1838000))
90
-    db.session.add(Band(name='160m', zone='iaru1', modes="SSB,AM", start=1838000))
91
-    db.session.add(Band(name='160m', zone='iaru1', modes=None, start=1810000))
92
-
93
-    db.session.add(Band(name='80m', zone='iaru1', modes="CW", start=3500000))
94
-    db.session.add(Band(name='80m', zone='iaru1', modes="RTTY,PSK31,PSK63,SSTV", start=3580000))
95
-    db.session.add(Band(name='80m', zone='iaru1', modes="SSB,AM", start=3600000))
96
-    db.session.add(Band(name='80m', zone='iaru1', modes=None, start=3500000))
97
-
98
-    db.session.add(Band(name='40m', zone='iaru1', modes="CW", start=7000000))
99
-    db.session.add(Band(name='40m', zone='iaru1', modes="RTTY,PSK31,PSK63,SSTV", start=7040000))
100
-    db.session.add(Band(name='40m', zone='iaru1', modes="SSB,AM", start=7060000))
101
-    db.session.add(Band(name='40m', zone='iaru1', modes=None, start=7000000))
102
-
103
-    db.session.add(Band(name='30m', zone='iaru1', modes="CW", start=10100000))
104
-    db.session.add(Band(name='30m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=10140000))
105
-    db.session.add(Band(name='30m', zone='iaru1', modes=None, start=10100000))
106
-
107
-    db.session.add(Band(name='20m', zone='iaru1', modes="CW", start=14000000))
108
-    db.session.add(Band(name='20m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=14070000))
109
-    db.session.add(Band(name='20m', zone='iaru1', modes="SSB,AM", start=14101000))
110
-    db.session.add(Band(name='20m', zone='iaru1', modes=None, start=14000000))
111
-
112
-    db.session.add(Band(name='17m', zone='iaru1', modes="CW", start=18068000))
113
-    db.session.add(Band(name='17m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=18095000))
114
-    db.session.add(Band(name='17m', zone='iaru1', modes="SSB,AM", start=18111000))
115
-    db.session.add(Band(name='17m', zone='iaru1', modes=None, start=18068000))
116
-
117
-    db.session.add(Band(name='15m', zone='iaru1', modes="CW", start=21000000))
118
-    db.session.add(Band(name='15m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=21070000))
119
-    db.session.add(Band(name='15m', zone='iaru1', modes="SSB,AM,SSTV", start=21151000))
120
-    db.session.add(Band(name='15m', zone='iaru1', modes=None, start=21000000))
121
-
122
-    db.session.add(Band(name='12m', zone='iaru1', modes="CW", start=24890000))
123
-    db.session.add(Band(name='12m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=24915000))
124
-    db.session.add(Band(name='12m', zone='iaru1', modes="SSB,AM,SSTV", start=24931000))
125
-    db.session.add(Band(name='12m', zone='iaru1', modes=None, start=24890000))
126
-
127
-    db.session.add(Band(name='10m', zone='iaru1', modes="CW", start=28000000))
128
-    db.session.add(Band(name='10m', zone='iaru1', modes="RTTY,PSK31,PSK63", start=28070000))
129
-    db.session.add(Band(name='10m', zone='iaru1', modes="SSB,AM,SSTV", start=28225000))
130
-    db.session.add(Band(name='10m', zone='iaru1', modes="FM,PKT", start=29200000))
131
-    db.session.add(Band(name='10m', zone='iaru1', modes=None, start=28000000))
132
-
133
-    db.session.add(Band(name='6m', zone='iaru1', modes="CW", start=50000000))
134
-    db.session.add(Band(name='6m', zone='iaru1', modes="SSB", start=50110000))
135
-    db.session.add(Band(name='6m', zone='iaru1', modes="SSTV", start=50510000))
136
-    db.session.add(Band(name='6m', zone='iaru1', modes="FM", start=50520000))
137
-    db.session.add(Band(name='6m', zone='iaru1', modes="RTTY", start=50600000))
138
-    db.session.add(Band(name='6m', zone='iaru1', modes=None, start=50000000))
139
-
140
-    db.session.add(Band(name='2m', zone='iaru1', modes="CW,SSB", start=144025000))
141
-    db.session.add(Band(name='2m', zone='iaru1', modes="FM", start=145000000))
142
-    db.session.add(Band(name='2m', zone='iaru1', modes=None, start=144000000))
143
-
144
-    db.session.add(Band(name='70cm', zone='iaru1', modes="FM", start=430250000))
145
-    db.session.add(Band(name='70cm', zone='iaru1', modes="CW,SSB", start=432000000))
146
-    db.session.add(Band(name='70cm', zone='iaru1', modes="PSK31", start=432088000))
147
-    db.session.add(Band(name='70cm', zone='iaru1', modes="SSTV", start=432500000))
148
-    db.session.add(Band(name='70cm', zone='iaru1', modes="RTTY", start=432562500))
149
-    db.session.add(Band(name='70cm', zone='iaru1', modes=None, start=430000000))
150
-
151
-    db.session.add(Band(name='23cm', zone='iaru1', modes="FM,SSB,CW", start=1260000000))
152
-    db.session.add(Band(name='23cm', zone='iaru1', modes="RTTY", start=1270000000))
153
-    db.session.add(Band(name='23cm', zone='iaru1', modes="PSK31", start=1296138000))
154
-    db.session.add(Band(name='23cm', zone='iaru1', modes=None, start=1240000000))
155
-
156
-    db.session.add(Band(name='13cm', zone='iaru1', modes="CW", start=2320025000))
157
-    db.session.add(Band(name='13cm', zone='iaru1', modes="PSK31", start=2320138000))
158
-    db.session.add(Band(name='13cm', zone='iaru1', modes="LSB", start=2320150000))
159
-    db.session.add(Band(name='13cm', zone='iaru1', modes="FM", start=2321000000))
160
-    db.session.add(Band(name='13cm', zone='iaru1', modes=None, start=2300000000))
161
-
162
-    db.session.add(Band(name='5cm', zone='iaru1', modes=None, start=5650000000))
163
-
164
-    db.session.add(Band(name='3cm', zone='iaru1', modes="CW,SSB", start=10368000000))
165
-    db.session.add(Band(name='3cm', zone='iaru1', modes=None, start=10000000000))
166
-
167
-    db.session.add(Band(name='1,2cm', zone='iaru1', modes=None, start=24000000000))
168
-
169
-    db.session.add(Band(name='6mm', zone='iaru1', modes=None, start=47000000000))
170
-
171
-    db.session.add(Band(name='4mm', zone='iaru1', modes=None, start=76000000000))
172
-
173
-    db.session.add(Band(name='2,4mm', zone='iaru1', modes=None, start=122250000000))
174
-
175
-    db.session.add(Band(name='2mm', zone='iaru1', modes=None, start=134000000000))
176
-
177
-    db.session.add(Band(name='1,2mm', zone='iaru1', modes=None, start=241000000000))
89
+    db.session.add(Band(name="2222m", zone="iaru1", modes=None, start=135700))
90
+
91
+    db.session.add(Band(name="160m", zone="iaru1", modes="CW", start=1810000))
92
+    db.session.add(Band(name="160m", zone="iaru1", modes="RTTY,PSK31,PSK63,SSTV", start=1838000))
93
+    db.session.add(Band(name="160m", zone="iaru1", modes="SSB,AM", start=1838000))
94
+    db.session.add(Band(name="160m", zone="iaru1", modes=None, start=1810000))
95
+
96
+    db.session.add(Band(name="80m", zone="iaru1", modes="CW", start=3500000))
97
+    db.session.add(Band(name="80m", zone="iaru1", modes="RTTY,PSK31,PSK63,SSTV", start=3580000))
98
+    db.session.add(Band(name="80m", zone="iaru1", modes="SSB,AM", start=3600000))
99
+    db.session.add(Band(name="80m", zone="iaru1", modes=None, start=3500000))
100
+
101
+    db.session.add(Band(name="40m", zone="iaru1", modes="CW", start=7000000))
102
+    db.session.add(Band(name="40m", zone="iaru1", modes="RTTY,PSK31,PSK63,SSTV", start=7040000))
103
+    db.session.add(Band(name="40m", zone="iaru1", modes="SSB,AM", start=7060000))
104
+    db.session.add(Band(name="40m", zone="iaru1", modes=None, start=7000000))
105
+
106
+    db.session.add(Band(name="30m", zone="iaru1", modes="CW", start=10100000))
107
+    db.session.add(Band(name="30m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=10140000))
108
+    db.session.add(Band(name="30m", zone="iaru1", modes=None, start=10100000))
109
+
110
+    db.session.add(Band(name="20m", zone="iaru1", modes="CW", start=14000000))
111
+    db.session.add(Band(name="20m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=14070000))
112
+    db.session.add(Band(name="20m", zone="iaru1", modes="SSB,AM", start=14101000))
113
+    db.session.add(Band(name="20m", zone="iaru1", modes=None, start=14000000))
114
+
115
+    db.session.add(Band(name="17m", zone="iaru1", modes="CW", start=18068000))
116
+    db.session.add(Band(name="17m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=18095000))
117
+    db.session.add(Band(name="17m", zone="iaru1", modes="SSB,AM", start=18111000))
118
+    db.session.add(Band(name="17m", zone="iaru1", modes=None, start=18068000))
119
+
120
+    db.session.add(Band(name="15m", zone="iaru1", modes="CW", start=21000000))
121
+    db.session.add(Band(name="15m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=21070000))
122
+    db.session.add(Band(name="15m", zone="iaru1", modes="SSB,AM,SSTV", start=21151000))
123
+    db.session.add(Band(name="15m", zone="iaru1", modes=None, start=21000000))
124
+
125
+    db.session.add(Band(name="12m", zone="iaru1", modes="CW", start=24890000))
126
+    db.session.add(Band(name="12m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=24915000))
127
+    db.session.add(Band(name="12m", zone="iaru1", modes="SSB,AM,SSTV", start=24931000))
128
+    db.session.add(Band(name="12m", zone="iaru1", modes=None, start=24890000))
129
+
130
+    db.session.add(Band(name="10m", zone="iaru1", modes="CW", start=28000000))
131
+    db.session.add(Band(name="10m", zone="iaru1", modes="RTTY,PSK31,PSK63", start=28070000))
132
+    db.session.add(Band(name="10m", zone="iaru1", modes="SSB,AM,SSTV", start=28225000))
133
+    db.session.add(Band(name="10m", zone="iaru1", modes="FM,PKT", start=29200000))
134
+    db.session.add(Band(name="10m", zone="iaru1", modes=None, start=28000000))
135
+
136
+    db.session.add(Band(name="6m", zone="iaru1", modes="CW", start=50000000))
137
+    db.session.add(Band(name="6m", zone="iaru1", modes="SSB", start=50110000))
138
+    db.session.add(Band(name="6m", zone="iaru1", modes="SSTV", start=50510000))
139
+    db.session.add(Band(name="6m", zone="iaru1", modes="FM", start=50520000))
140
+    db.session.add(Band(name="6m", zone="iaru1", modes="RTTY", start=50600000))
141
+    db.session.add(Band(name="6m", zone="iaru1", modes=None, start=50000000))
142
+
143
+    db.session.add(Band(name="2m", zone="iaru1", modes="CW,SSB", start=144025000))
144
+    db.session.add(Band(name="2m", zone="iaru1", modes="FM", start=145000000))
145
+    db.session.add(Band(name="2m", zone="iaru1", modes=None, start=144000000))
146
+
147
+    db.session.add(Band(name="70cm", zone="iaru1", modes="FM", start=430250000))
148
+    db.session.add(Band(name="70cm", zone="iaru1", modes="CW,SSB", start=432000000))
149
+    db.session.add(Band(name="70cm", zone="iaru1", modes="PSK31", start=432088000))
150
+    db.session.add(Band(name="70cm", zone="iaru1", modes="SSTV", start=432500000))
151
+    db.session.add(Band(name="70cm", zone="iaru1", modes="RTTY", start=432562500))
152
+    db.session.add(Band(name="70cm", zone="iaru1", modes=None, start=430000000))
153
+
154
+    db.session.add(Band(name="23cm", zone="iaru1", modes="FM,SSB,CW", start=1260000000))
155
+    db.session.add(Band(name="23cm", zone="iaru1", modes="RTTY", start=1270000000))
156
+    db.session.add(Band(name="23cm", zone="iaru1", modes="PSK31", start=1296138000))
157
+    db.session.add(Band(name="23cm", zone="iaru1", modes=None, start=1240000000))
158
+
159
+    db.session.add(Band(name="13cm", zone="iaru1", modes="CW", start=2320025000))
160
+    db.session.add(Band(name="13cm", zone="iaru1", modes="PSK31", start=2320138000))
161
+    db.session.add(Band(name="13cm", zone="iaru1", modes="LSB", start=2320150000))
162
+    db.session.add(Band(name="13cm", zone="iaru1", modes="FM", start=2321000000))
163
+    db.session.add(Band(name="13cm", zone="iaru1", modes=None, start=2300000000))
164
+
165
+    db.session.add(Band(name="5cm", zone="iaru1", modes=None, start=5650000000))
166
+
167
+    db.session.add(Band(name="3cm", zone="iaru1", modes="CW,SSB", start=10368000000))
168
+    db.session.add(Band(name="3cm", zone="iaru1", modes=None, start=10000000000))
169
+
170
+    db.session.add(Band(name="1,2cm", zone="iaru1", modes=None, start=24000000000))
171
+
172
+    db.session.add(Band(name="6mm", zone="iaru1", modes=None, start=47000000000))
173
+
174
+    db.session.add(Band(name="4mm", zone="iaru1", modes=None, start=76000000000))
175
+
176
+    db.session.add(Band(name="2,4mm", zone="iaru1", modes=None, start=122250000000))
177
+
178
+    db.session.add(Band(name="2mm", zone="iaru1", modes=None, start=134000000000))
179
+
180
+    db.session.add(Band(name="1,2mm", zone="iaru1", modes=None, start=241000000000))
178 181
 
179 182
     db.session.commit()

+ 164
- 157
forms.py View File

@@ -5,8 +5,7 @@ from flask_security import RegisterForm, current_user
5 5
 from flask_uploads import UploadSet, IMAGES
6 6
 from flask_wtf import FlaskForm as Form
7 7
 from flask_wtf.file import FileField, FileAllowed, FileRequired
8
-from wtforms import PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, \
9
-    HiddenField, BooleanField
8
+from wtforms import PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, HiddenField, BooleanField
10 9
 from wtforms.ext.dateutil.fields import DateTimeField
11 10
 from wtforms.ext.sqlalchemy.fields import QuerySelectField
12 11
 from wtforms.validators import DataRequired, ValidationError, Optional
@@ -20,7 +19,7 @@ from utils import dt_utc_to_user_tz
20 19
 
21 20
 BaseModelForm = model_form_factory(Form)
22 21
 
23
-pictures = UploadSet('pictures', IMAGES)
22
+pictures = UploadSet("pictures", IMAGES)
24 23
 
25 24
 # monkeypatch for https://github.com/wtforms/wtforms/issues/373
26 25
 def _patch_wtforms_sqlalchemy():
@@ -29,7 +28,7 @@ def _patch_wtforms_sqlalchemy():
29 28
 
30 29
     def get_pk_from_identity(obj):
31 30
         key = identity_key(instance=obj)[1]
32
-        return u':'.join(map(str, key))
31
+        return ":".join(map(str, key))
33 32
 
34 33
     fields.get_pk_from_identity = get_pk_from_identity
35 34
 
@@ -37,6 +36,7 @@ def _patch_wtforms_sqlalchemy():
37 36
 _patch_wtforms_sqlalchemy()
38 37
 del _patch_wtforms_sqlalchemy
39 38
 
39
+
40 40
 class PasswordFieldNotHidden(StringField):
41 41
     """
42 42
     Original source: https://github.com/wtforms/wtforms/blob/2.0.2/wtforms/fields/simple.py#L35-L42
@@ -45,6 +45,7 @@ class PasswordFieldNotHidden(StringField):
45 45
     Also, whatever value is accepted by this field is not rendered back
46 46
     to the browser like normal fields.
47 47
     """
48
+
48 49
     widget = widgets.PasswordInput(hide_value=False)
49 50
 
50 51
 
@@ -55,7 +56,7 @@ class ModelForm(BaseModelForm):
55 56
 
56 57
 
57 58
 class ExtendedRegisterForm(RegisterForm):
58
-    name = StringField('Name', [DataRequired()])
59
+    name = StringField("Name", [DataRequired()])
59 60
 
60 61
     def validate_name(form, field):
61 62
         if len(field.data) <= 0:
@@ -70,13 +71,13 @@ class UserProfileForm(ModelForm):
70 71
     class Meta:
71 72
         model = User
72 73
 
73
-    password = PasswordField('Password')
74
-    name = StringField('Name')
75
-    email = StringField('Email')
74
+    password = PasswordField("Password")
75
+    name = StringField("Name")
76
+    email = StringField("Email")
76 77
 
77
-    callsign = StringField('Callsign', [DataRequired()])
78
+    callsign = StringField("Callsign", [DataRequired()])
78 79
 
79
-    locator = StringField('Locator', [DataRequired()])
80
+    locator = StringField("Locator", [DataRequired()])
80 81
 
81 82
     def validate_locator(form, field):
82 83
         if len(field.data) <= 2:
@@ -84,37 +85,40 @@ class UserProfileForm(ModelForm):
84 85
         if not is_valid_qth(field.data, 6):
85 86
             raise ValidationError("QTH is invalid, validation failed")
86 87
 
87
-    firstname = StringField('Firstname')
88
-    lastname = StringField('Lastname')
89
-    timezone = SelectField(coerce=str, label='Timezone', default='UTC')
90
-    lotw_name = StringField('LoTW Username')
91
-    lotw_password = PasswordFieldNotHidden('LoTW Password')
92
-    eqsl_name = StringField('eQSL.cc Username')
93
-    eqsl_password = PasswordFieldNotHidden('eQSL.cc Password')
94
-    hamqth_name = StringField('HamQTH Username')
95
-    hamqth_password = PasswordFieldNotHidden('HamQTH Password')
88
+    firstname = StringField("Firstname")
89
+    lastname = StringField("Lastname")
90
+    timezone = SelectField(coerce=str, label="Timezone", default="UTC")
91
+    lotw_name = StringField("LoTW Username")
92
+    lotw_password = PasswordFieldNotHidden("LoTW Password")
93
+    eqsl_name = StringField("eQSL.cc Username")
94
+    eqsl_password = PasswordFieldNotHidden("eQSL.cc Password")
95
+    hamqth_name = StringField("HamQTH Username")
96
+    hamqth_password = PasswordFieldNotHidden("HamQTH Password")
96 97
 
97
-    swl = BooleanField('Are you a SWL HAM ?')
98
+    swl = BooleanField("Are you a SWL HAM ?")
98 99
 
99
-    zone = SelectField('Zone', choices=[['iaru1', 'IARU Zone 1'],
100
-                                        ['iaru2', 'IARU Zone 2'],
101
-                                        ['iaru3', 'IARU Zone 3']], validators=[DataRequired()])
100
+    zone = SelectField(
101
+        "Zone",
102
+        choices=[["iaru1", "IARU Zone 1"], ["iaru2", "IARU Zone 2"], ["iaru3", "IARU Zone 3"]],
103
+        validators=[DataRequired()],
104
+    )
102 105
 
103
-    submit = SubmitField('Update profile')
106
+    submit = SubmitField("Update profile")
104 107
 
105 108
 
106 109
 class NoteForm(ModelForm):
107 110
     class Meta:
108 111
         model = Note
109 112
 
110
-    cat = SelectField(choices=[
111
-        ('General', 'General'),
112
-        ('Antennas', 'Antennas'),
113
-        ('Satellites', 'Satellites')], default=['General'], label='Category')
114
-    title = StringField('Title', [DataRequired()])
115
-    note = TextAreaField('Note', [DataRequired()])
113
+    cat = SelectField(
114
+        choices=[("General", "General"), ("Antennas", "Antennas"), ("Satellites", "Satellites")],
115
+        default=["General"],
116
+        label="Category",
117
+    )
118
+    title = StringField("Title", [DataRequired()])
119
+    note = TextAreaField("Note", [DataRequired()])
116 120
 
117
-    submit = SubmitField('Save note')
121
+    submit = SubmitField("Save note")
118 122
 
119 123
 
120 124
 def get_modes():
@@ -129,26 +133,41 @@ def get_modes():
129 133
 
130 134
 
131 135
 def get_bands():
132
-    return Band.query.filter(Band.modes.is_(None),
133
-                             Band.start.is_(None),
134
-                             Band.zone == current_user.zone).order_by(Band.lower.asc()).all()
136
+    return (
137
+        Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.zone == current_user.zone)
138
+        .order_by(Band.lower.asc())
139
+        .all()
140
+    )
135 141
 
136 142
 
137 143
 def dflt_mode():
138
-    return str(Mode.query.filter(Mode.submode == 'LSB').first())
144
+    return str(Mode.query.filter(Mode.submode == "LSB").first())
139 145
 
140 146
 
141 147
 def dflt_band():
142
-    return Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.name == '40m').first()
143
-
144
-
145
-list_of_props = [['', ''], ['AUR', 'Aurora'], ['AUE', 'Aurora-E'], ['BS', 'Back scatter'],
146
-                 ['ECH', 'EchoLink'], ['EME', 'Earth-Moon-Earth'], ['ES', 'Sporadic E'],
147
-                 ['FAI', 'Field Aligned Irregularities'], ['F2', 'F2 Reflection'],
148
-                 ['INTERNET', 'Internet-assisted'], ['ION', 'Ionoscatter'], ['IRL', 'IRLP'],
149
-                 ['MS', 'Meteor scatter'], ['RPT', 'Terrestrial or atmospheric repeater or transponder'],
150
-                 ['RS', 'Rain scatter'], ['SAT', 'Satellite'], ['TEP', 'Trans-equatorial'],
151
-                 ['TR', 'Tropospheric ducting']]
148
+    return Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.name == "40m").first()
149
+
150
+
151
+list_of_props = [
152
+    ["", ""],
153
+    ["AUR", "Aurora"],
154
+    ["AUE", "Aurora-E"],
155
+    ["BS", "Back scatter"],
156
+    ["ECH", "EchoLink"],
157
+    ["EME", "Earth-Moon-Earth"],
158
+    ["ES", "Sporadic E"],
159
+    ["FAI", "Field Aligned Irregularities"],
160
+    ["F2", "F2 Reflection"],
161
+    ["INTERNET", "Internet-assisted"],
162
+    ["ION", "Ionoscatter"],
163
+    ["IRL", "IRLP"],
164
+    ["MS", "Meteor scatter"],
165
+    ["RPT", "Terrestrial or atmospheric repeater or transponder"],
166
+    ["RS", "Rain scatter"],
167
+    ["SAT", "Satellite"],
168
+    ["TEP", "Trans-equatorial"],
169
+    ["TR", "Tropospheric ducting"],
170
+]
152 171
 
153 172
 
154 173
 def get_radios():
@@ -167,16 +186,17 @@ class BaseQsoForm(Form):
167 186
     # Hardcoded value for mode default
168 187
     # WTFORMS-Components doesn't seems to be able to manage callable for default= unfortunately
169 188
     # We use 1 which should be the first Mode ID in database (LSB)
170
-    call = StringField('Callsign', [DataRequired()])
171
-    mode = WTFComponentsSelectField('Mode', choices=get_modes, validators=[DataRequired()], coerce=int, default='1')
172
-    band = QuerySelectField(query_factory=get_bands, default=dflt_band, label='Band',
173
-                            validators=[DataRequired()], get_label='name')
174
-    rst_sent = IntegerField('RST (S)', [DataRequired()], default=59)
175
-    rst_rcvd = IntegerField('RST (R)', [DataRequired()], default=59)
176
-    name = StringField('Name')
177
-    qth = StringField('Location')
178
-
179
-    gridsquare = StringField('Locator')
189
+    call = StringField("Callsign", [DataRequired()])
190
+    mode = WTFComponentsSelectField("Mode", choices=get_modes, validators=[DataRequired()], coerce=int, default="1")
191
+    band = QuerySelectField(
192
+        query_factory=get_bands, default=dflt_band, label="Band", validators=[DataRequired()], get_label="name"
193
+    )
194
+    rst_sent = IntegerField("RST (S)", [DataRequired()], default=59)
195
+    rst_rcvd = IntegerField("RST (R)", [DataRequired()], default=59)
196
+    name = StringField("Name")
197
+    qth = StringField("Location")
198
+
199
+    gridsquare = StringField("Locator")
180 200
 
181 201
     def validate_gridsquare(form, field):
182 202
         if len(field.data) <= 0:
@@ -187,107 +207,95 @@ class BaseQsoForm(Form):
187 207
         if not is_valid_qth(field.data, 6):
188 208
             raise ValidationError("QTH is invalid, validation failed")
189 209
 
190
-    comment = StringField('Comment')
191
-    qsl_comment = StringField('QSL Comment')
192
-    country = StringField('Country', [DataRequired()])
210
+    comment = StringField("Comment")
211
+    qsl_comment = StringField("QSL Comment")
212
+    country = StringField("Country", [DataRequired()])
193 213
 
194
-    web = StringField('URL')
214
+    web = StringField("URL")
195 215
 
196 216
     # Hidden
197 217
     dxcc = HiddenField(validators=[DataRequired()])
198 218
     cqz = HiddenField(validators=[DataRequired()])
199 219
 
200 220
     # Home
201
-    prop_mode = SelectField(choices=list_of_props, default='', label='Propagation Mode')
202
-    iota = StringField('IOTA')
221
+    prop_mode = SelectField(choices=list_of_props, default="", label="Propagation Mode")
222
+    iota = StringField("IOTA")
203 223
 
204 224
     # Station
205
-    radio = QuerySelectField(query_factory=get_radios, allow_blank=True, label='Radio', get_label='radio')
206
-    freq = IntegerField('Frequency', [DataRequired()])
225
+    radio = QuerySelectField(query_factory=get_radios, allow_blank=True, label="Radio", get_label="radio")
226
+    freq = IntegerField("Frequency", [DataRequired()])
207 227
 
208 228
     # Satellite
209
-    sat_name = StringField('Sat name')
210
-    sat_mode = StringField('Sat mode')
229
+    sat_name = StringField("Sat name")
230
+    sat_mode = StringField("Sat mode")
211 231
 
212 232
     # QSL
213
-    qsl_sent = SelectField('QSL Sent', choices=[['N', 'No'],
214
-                                                ['Y', 'Yes'],
215
-                                                ['R', 'Requested'],
216
-                                                ['Q', 'Queued'],
217
-                                                ['I', 'Invalid (Ignore)']])
218
-    qsl_sent_via = SelectField('Sent via', choices=[['', 'Method'],
219
-                                                    ['D', 'Direct'],
220
-                                                    ['B', 'Bureau'],
221
-                                                    ['E', 'Electronic'],
222
-                                                    ['M', 'Manager']])
223
-    eqsl_qsl_sent = SelectField('eQSL Sent', choices=[['N', 'No'],