Another Ham Radio Logbook -- Web, Multi-user multiple-logbook, with eQSL upload support
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

forms.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import datetime
  2. from libqth import is_valid_qth
  3. from flask_security import RegisterForm, current_user
  4. from flask_uploads import UploadSet, IMAGES
  5. from flask_wtf import FlaskForm as Form
  6. from flask_wtf.file import FileField, FileAllowed, FileRequired
  7. from wtforms import PasswordField, SubmitField, TextAreaField, SelectField, IntegerField, HiddenField, BooleanField
  8. from wtforms.ext.dateutil.fields import DateTimeField
  9. from wtforms.ext.sqlalchemy.fields import QuerySelectField
  10. from wtforms.validators import DataRequired, ValidationError, Optional
  11. from wtforms_alchemy import model_form_factory
  12. from wtforms_components.fields import SelectField as WTFComponentsSelectField
  13. from wtforms import widgets
  14. from wtforms.fields.core import StringField
  15. from models import db, User, Cat, Mode, Band, Logbook
  16. from utils import dt_utc_to_user_tz
  17. BaseModelForm = model_form_factory(Form)
  18. pictures = UploadSet("pictures", IMAGES)
  19. # monkeypatch for https://github.com/wtforms/wtforms/issues/373
  20. def _patch_wtforms_sqlalchemy():
  21. from wtforms.ext.sqlalchemy import fields
  22. from sqlalchemy.orm.util import identity_key
  23. def get_pk_from_identity(obj):
  24. key = identity_key(instance=obj)[1]
  25. return ":".join(map(str, key))
  26. fields.get_pk_from_identity = get_pk_from_identity
  27. _patch_wtforms_sqlalchemy()
  28. del _patch_wtforms_sqlalchemy
  29. class PasswordFieldNotHidden(StringField):
  30. """
  31. Original source: https://github.com/wtforms/wtforms/blob/2.0.2/wtforms/fields/simple.py#L35-L42
  32. A StringField, except renders an ``<input type="password">``.
  33. Also, whatever value is accepted by this field is not rendered back
  34. to the browser like normal fields.
  35. """
  36. widget = widgets.PasswordInput(hide_value=False)
  37. class ModelForm(BaseModelForm):
  38. @classmethod
  39. def get_session(cls):
  40. return db.session
  41. class ExtendedRegisterForm(RegisterForm):
  42. name = StringField("Name", [DataRequired()])
  43. def validate_name(form, field):
  44. if len(field.data) <= 0:
  45. raise ValidationError("Username required")
  46. u = User.query.filter(User.name == field.data).first()
  47. if u:
  48. raise ValidationError("Username already taken")
  49. class UserProfileForm(ModelForm):
  50. class Meta:
  51. model = User
  52. password = PasswordField("Password")
  53. name = StringField("Name")
  54. email = StringField("Email")
  55. callsign = StringField("Callsign", [DataRequired()])
  56. locator = StringField("Locator", [DataRequired()])
  57. def validate_locator(form, field):
  58. if len(field.data) <= 2:
  59. raise ValidationError("QTH is too broad, please input valid QTH")
  60. if not is_valid_qth(field.data, 6):
  61. raise ValidationError("QTH is invalid, validation failed")
  62. firstname = StringField("Firstname")
  63. lastname = StringField("Lastname")
  64. timezone = SelectField(coerce=str, label="Timezone", default="UTC")
  65. lotw_name = StringField("LoTW Username")
  66. lotw_password = PasswordFieldNotHidden("LoTW Password")
  67. eqsl_name = StringField("eQSL.cc Username")
  68. eqsl_password = PasswordFieldNotHidden("eQSL.cc Password")
  69. hamqth_name = StringField("HamQTH Username")
  70. hamqth_password = PasswordFieldNotHidden("HamQTH Password")
  71. swl = BooleanField("Are you a SWL HAM ?")
  72. zone = SelectField(
  73. "Zone",
  74. choices=[["iaru1", "IARU Zone 1"], ["iaru2", "IARU Zone 2"], ["iaru3", "IARU Zone 3"]],
  75. validators=[DataRequired()],
  76. )
  77. submit = SubmitField("Update profile")
  78. class NoteForm(Form):
  79. title = StringField("Title", [DataRequired()])
  80. cat = SelectField(
  81. choices=[("General", "General"), ("Antennas", "Antennas"), ("Satellites", "Satellites")],
  82. default=["General"],
  83. label="Category",
  84. )
  85. note = TextAreaField("Note", [DataRequired()])
  86. submit = SubmitField("Save note")
  87. def get_modes():
  88. modes = {}
  89. q_modes = db.session.query(Mode.id, Mode.mode, Mode.submode).order_by(Mode.mode.asc()).all()
  90. for mode in q_modes:
  91. if mode.mode not in modes:
  92. modes[mode.mode] = []
  93. modes[mode.mode].append((mode.id, mode.submode))
  94. return list(sorted(modes.items()))
  95. def get_bands():
  96. return (
  97. Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.zone == current_user.zone)
  98. .order_by(Band.lower.asc())
  99. .all()
  100. )
  101. def dflt_mode():
  102. return str(Mode.query.filter(Mode.submode == "LSB").first())
  103. def dflt_band():
  104. return Band.query.filter(Band.modes.is_(None), Band.start.is_(None), Band.name == "40m").first()
  105. list_of_props = [
  106. ["", ""],
  107. ["AUR", "Aurora"],
  108. ["AUE", "Aurora-E"],
  109. ["BS", "Back scatter"],
  110. ["ECH", "EchoLink"],
  111. ["EME", "Earth-Moon-Earth"],
  112. ["ES", "Sporadic E"],
  113. ["FAI", "Field Aligned Irregularities"],
  114. ["F2", "F2 Reflection"],
  115. ["INTERNET", "Internet-assisted"],
  116. ["ION", "Ionoscatter"],
  117. ["IRL", "IRLP"],
  118. ["MS", "Meteor scatter"],
  119. ["RPT", "Terrestrial or atmospheric repeater or transponder"],
  120. ["RS", "Rain scatter"],
  121. ["SAT", "Satellite"],
  122. ["TEP", "Trans-equatorial"],
  123. ["TR", "Tropospheric ducting"],
  124. ]
  125. def get_radios():
  126. return Cat.query.all()
  127. def foo_bar_baz_qux():
  128. return dt_utc_to_user_tz(datetime.datetime.utcnow())
  129. def get_logbooks():
  130. return Logbook.query.filter(Logbook.user_id == current_user.id).all()
  131. class BaseQsoForm(Form):
  132. # Hardcoded value for mode default
  133. # WTFORMS-Components doesn't seems to be able to manage callable for default= unfortunately
  134. # We use 1 which should be the first Mode ID in database (LSB)
  135. call = StringField("Callsign", [DataRequired()])
  136. mode = WTFComponentsSelectField("Mode", choices=get_modes, validators=[DataRequired()], coerce=int, default="1")
  137. band = QuerySelectField(
  138. query_factory=get_bands, default=dflt_band, label="Band", validators=[DataRequired()], get_label="name"
  139. )
  140. rst_sent = IntegerField("RST (S)", [DataRequired()], default=59)
  141. rst_rcvd = IntegerField("RST (R)", [DataRequired()], default=59)
  142. name = StringField("Name")
  143. qth = StringField("Location")
  144. gridsquare = StringField("Locator")
  145. def validate_gridsquare(form, field):
  146. if len(field.data) <= 0:
  147. return # ignore if no QTH entered
  148. if len(field.data) <= 2:
  149. raise ValidationError("QTH is too broad, please input valid QTH")
  150. if not is_valid_qth(field.data, 6):
  151. raise ValidationError("QTH is invalid, validation failed")
  152. comment = StringField("Comment")
  153. qsl_comment = StringField("QSL Comment")
  154. country = StringField("Country", [DataRequired()])
  155. web = StringField("URL")
  156. # Hidden
  157. dxcc = HiddenField(validators=[DataRequired()])
  158. cqz = HiddenField(validators=[DataRequired()])
  159. # Home
  160. prop_mode = SelectField(choices=list_of_props, default="", label="Propagation Mode")
  161. iota = StringField("IOTA")
  162. # Station
  163. radio = QuerySelectField(query_factory=get_radios, allow_blank=True, label="Radio", get_label="radio")
  164. freq = IntegerField("Frequency", [DataRequired()])
  165. # Satellite
  166. sat_name = StringField("Sat name")
  167. sat_mode = StringField("Sat mode")
  168. # QSL
  169. qsl_sent = SelectField(
  170. "QSL Sent", choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["Q", "Queued"], ["I", "Invalid (Ignore)"]]
  171. )
  172. qsl_sent_via = SelectField(
  173. "Sent via", choices=[["", "Method"], ["D", "Direct"], ["B", "Bureau"], ["E", "Electronic"], ["M", "Manager"]]
  174. )
  175. eqsl_qsl_sent = SelectField(
  176. "eQSL Sent", choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["Q", "Queued"], ["I", "Invalid (Ignore)"]]
  177. )
  178. qsl_via = StringField("Via")
  179. submit = SubmitField("Save")
  180. class QsoForm(BaseQsoForm):
  181. date = DateTimeField("Date", default=datetime.datetime.utcnow, display_format="%Y-%m-%d")
  182. time = DateTimeField("Time", default=foo_bar_baz_qux, display_format="%H:%M:%S")
  183. class EditQsoForm(BaseQsoForm):
  184. time_on = DateTimeField("Start date", display_format="%Y-%m-%d %H:%M:%S", validators=[DataRequired()])
  185. time_off = DateTimeField("End date", display_format="%Y-%m-%d %H:%M:%S", validators=[DataRequired()])
  186. notes = TextAreaField("Notes")
  187. qsl_rcvd = SelectField(
  188. "QSL Received",
  189. choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["I", "Invalid (Ignore)"], ["V", "Verified (Match)"]],
  190. )
  191. qsl_rcvd_via = SelectField(
  192. "Received via",
  193. choices=[["", "Method"], ["D", "Direct"], ["B", "Bureau"], ["E", "Electronic"], ["M", "Manager"]],
  194. )
  195. eqsl_qsl_rcvd = SelectField(
  196. "eQSL Received",
  197. choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["I", "Invalid (Ignore)"], ["V", "Verified (Match)"]],
  198. )
  199. lotw_qsl_rcvd = SelectField(
  200. "LOTW QSL Received",
  201. choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["I", "Invalid (Ignore)"], ["V", "Verified (Match)"]],
  202. )
  203. lotw_qsl_sent = SelectField(
  204. "LOTW QSL Sent",
  205. choices=[["N", "No"], ["Y", "Yes"], ["R", "Requested"], ["Q", "Queued"], ["I", "Invalid (Ignore)"]],
  206. )
  207. class AdifParse(Form):
  208. logbook = QuerySelectField(query_factory=get_logbooks, allow_blank=False, label="Logbook", get_label="name")
  209. adif_file = FileField("File", [FileRequired(), FileAllowed(["adi", "adif"], "Adif only !")])
  210. submit = SubmitField("Import file")
  211. class FilterLogbookBandMode(Form):
  212. band = SelectField(label="Band", validators=[DataRequired()])
  213. mode = SelectField(label="Mode", validators=[DataRequired()])
  214. submit = SubmitField("Filter")
  215. class ContactsForm(Form):
  216. callsign = StringField("Callsign", [DataRequired()])
  217. gridsquare = StringField("Locator")
  218. def validate_gridsquare(form, field):
  219. if len(field.data) <= 2:
  220. raise ValidationError("QTH is too broad or empty, please input valid QTH")
  221. if not is_valid_qth(field.data, 6):
  222. raise ValidationError("QTH is invalid, validation failed")
  223. submit = SubmitField("Save contact")
  224. class BaseLogbookForm(Form):
  225. name = StringField("Name", [DataRequired()])
  226. callsign = StringField("Callsign", [DataRequired()])
  227. locator = StringField("Locator")
  228. eqsl_qth_nickname = StringField("eQSL QTH Nickname")
  229. swl = BooleanField("Logbook of a SWL HAM ?")
  230. public = BooleanField("Make this logbook public ?")
  231. default = BooleanField("Do you want this logbook to be the default one ?")
  232. def validate_gridsquare(form, field):
  233. if len(field.data) <= 2:
  234. raise ValidationError("QTH is too broad or empty, please input valid QTH")
  235. if not is_valid_qth(field.data, 6):
  236. raise ValidationError("QTH is invalid, validation failed")
  237. class LogbookForm(BaseLogbookForm):
  238. submit = SubmitField("Save logbook")
  239. class EditLogbookForm(BaseLogbookForm):
  240. old = BooleanField("Old and unused logbook ?")
  241. submit = SubmitField("Save logbook")
  242. class PictureForm(Form):
  243. name = StringField("Name", [DataRequired()])
  244. picture = FileField("Image", [FileRequired(), FileAllowed(pictures, "Images only!")])
  245. submit = SubmitField("Add picture")
  246. class ConfigForm(Form):
  247. clublog_api_key = StringField("ClubLog API Key")
  248. lotw_login_url = StringField("LORW Login URL", [DataRequired()])
  249. lotw_download_url = StringField("LOTW Download URL", [DataRequired()])
  250. lotw_upload_url = StringField("LOTW Upload URL", [DataRequired()])
  251. lotw_rcvd_mark = StringField("LOTW Rcvd Mark", [DataRequired()])
  252. eqsl_download_url = StringField("eQSL Download URL", [DataRequired()])
  253. eqsl_upload_url = StringField("eQSL Upload URL", [DataRequired()])
  254. eqsl_rcvd_mark = StringField("eQSL Rcvd Mark", [DataRequired()])
  255. submit = SubmitField("Update config")
  256. class AdvSearchForm(Form):
  257. # Remember that the Full Text Search already search text within:
  258. # Call, Comment, Country, Email, Name, Notes, Operator,
  259. # Owner callsign, Qslmsg, Station callsign, Web and Qsl comment
  260. # Select inputs will have a first field "Any" value "any"
  261. from_date = DateTimeField("From date", [Optional()])
  262. to_date = DateTimeField("To date", [Optional()])
  263. fts = StringField("Search string", [Optional()])
  264. country = SelectField(label="Country", validators=[Optional()])
  265. call = StringField("Callsign", [Optional()])
  266. mode = SelectField(label="Mode", validators=[Optional()])
  267. band = SelectField(label="Band", validators=[Optional()])
  268. frequency = IntegerField("Freq", [Optional()])
  269. pictures = SelectField(
  270. label="Has pictures", validators=[Optional()], choices=[["any", "Any"], ["Y", "Yes"], ["N", "No"]]
  271. )
  272. # qsl statues <select>
  273. # eqsl statues <select>
  274. # pictures <select>
  275. submit = SubmitField("Search")