Coverage for backend / app / data_tables / routers.py: 90%

41 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-17 21:34 +0000

1"""Module for generating CRUD routers for the JAM data tables""" 

2 

3import base64 

4 

5from fastapi import Depends, status, HTTPException, Response 

6from sqlalchemy.orm import Session 

7 

8from app import models, database 

9from app.core import oauth2 

10from app.data_tables import schemas 

11from app.geolocation.geolocation import geocode_location 

12from app.routers.utility import generate_data_table_crud_router 

13 

14# ---------------------------------------------------- SIMPLE TABLES --------------------------------------------------- 

15 

16# Keyword router 

17keyword_router = generate_data_table_crud_router( 

18 table_model=models.Keyword, 

19 create_schema=schemas.KeywordCreate, 

20 update_schema=schemas.KeywordUpdate, 

21 out_schema=schemas.KeywordOut, 

22 endpoint="keywords", 

23 not_found_msg="Keyword not found", 

24) 

25 

26# Aggregator router 

27aggregator_router = generate_data_table_crud_router( 

28 table_model=models.Aggregator, 

29 create_schema=schemas.AggregatorCreate, 

30 update_schema=schemas.AggregatorUpdate, 

31 out_schema=schemas.AggregatorOut, 

32 endpoint="aggregators", 

33 not_found_msg="Aggregator not found", 

34) 

35 

36# Company router 

37company_router = generate_data_table_crud_router( 

38 table_model=models.Company, 

39 create_schema=schemas.CompanyCreate, 

40 update_schema=schemas.CompanyUpdate, 

41 out_schema=schemas.CompanyOut, 

42 endpoint="companies", 

43 not_found_msg="Company not found", 

44) 

45 

46 

47# Location router 

48def transform_location(location_data: dict, db: Session, entry_data: dict | None = None) -> dict: 

49 """Geolocate the location data before creating/updating the record. 

50 :param location_data: The location data dictionary. 

51 :param db: The database session. 

52 :param entry_data: optional original data of the entry 

53 :return: The transformed location data dictionary with geolocation_id set.""" 

54 

55 if entry_data: 

56 location_data = location_data.copy() 

57 location_data.update(entry_data) 

58 params = { 

59 "postcode": location_data.get("postcode"), 

60 "city": location_data.get("city"), 

61 "country": location_data.get("country"), 

62 } 

63 geolocation = geocode_location(params, db) if params else None 

64 return {"geolocation_id": geolocation.id if geolocation else None} 

65 

66 

67location_router = generate_data_table_crud_router( 

68 table_model=models.Location, 

69 create_schema=schemas.LocationCreate, 

70 update_schema=schemas.LocationUpdate, 

71 out_schema=schemas.LocationOut, 

72 endpoint="locations", 

73 not_found_msg="Location not found", 

74 transform=transform_location, 

75) 

76 

77 

78# File router 

79file_router = generate_data_table_crud_router( 

80 table_model=models.File, 

81 create_schema=schemas.FileCreate, 

82 update_schema=schemas.FileUpdate, 

83 out_schema=schemas.FileOut, 

84 endpoint="files", 

85 not_found_msg="File not found", 

86) 

87 

88 

89@file_router.get("/{file_id}/download") 

90def download_file( 

91 file_id: int, 

92 db: Session = Depends(database.get_db), 

93 current_user: models.User = Depends(oauth2.get_current_user), 

94): 

95 """Download a file by ID. 

96 :param file_id: The file ID. 

97 :param db: The database session. 

98 :param current_user: The current user.""" 

99 

100 # Get file record from the database 

101 file_record = ( 

102 db.query(models.File).filter(models.File.id == file_id, models.File.owner_id == current_user.id).first() 

103 ) 

104 

105 if not file_record: 

106 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found") 

107 

108 if not file_record.content: 

109 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File content not found") 

110 

111 try: 

112 # Content is already a base64 string - just decode it 

113 if file_record.content.startswith("data:"): 

114 encoded_data = file_record.content.split(",", 1)[1] 

115 file_content = base64.b64decode(encoded_data) 

116 else: 

117 # Pure base64 string 

118 file_content = base64.b64decode(file_record.content) 

119 

120 except Exception as e: 

121 raise HTTPException( 

122 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error decoding file content: {str(e)}" 

123 ) 

124 

125 content_type = file_record.type if file_record.type else "application/octet-stream" 

126 

127 return Response( 

128 content=file_content, 

129 media_type=content_type, 

130 headers={ 

131 "Content-Disposition": f'attachment; filename="{file_record.filename}"', 

132 "Content-Length": str(len(file_content)), 

133 }, 

134 ) 

135 

136 

137# --------------------------------------------------- COMPLEX TABLES --------------------------------------------------- 

138 

139 

140# Person router 

141person_router = generate_data_table_crud_router( 

142 table_model=models.Person, 

143 create_schema=schemas.PersonCreate, 

144 update_schema=schemas.PersonUpdate, 

145 out_schema=schemas.PersonOut, 

146 endpoint="persons", 

147 not_found_msg="Person not found", 

148) 

149 

150# Job router 

151job_router = generate_data_table_crud_router( 

152 table_model=models.Job, 

153 create_schema=schemas.JobCreate, 

154 update_schema=schemas.JobUpdate, 

155 out_schema=schemas.JobOut, 

156 endpoint="jobs", 

157 not_found_msg="Job not found", 

158 many_to_many_fields={ 

159 "keywords": { 

160 "table": models.job_keyword_mapping, 

161 "local_key": "job_id", 

162 "remote_key": "keyword_id", 

163 "related_model": models.Keyword, 

164 }, 

165 "contacts": { 

166 "table": models.job_contact_mapping, 

167 "local_key": "job_id", 

168 "remote_key": "person_id", 

169 "related_model": models.Person, 

170 }, 

171 }, 

172) 

173 

174# Interview router 

175interview_router = generate_data_table_crud_router( 

176 table_model=models.Interview, 

177 create_schema=schemas.InterviewCreate, 

178 update_schema=schemas.InterviewUpdate, 

179 out_schema=schemas.InterviewOut, 

180 endpoint="interviews", 

181 not_found_msg="Interview not found", 

182 many_to_many_fields={ 

183 "interviewers": { 

184 "table": models.interview_interviewer_mapping, 

185 "local_key": "interview_id", 

186 "remote_key": "person_id", 

187 "related_model": models.Person, 

188 }, 

189 }, 

190) 

191 

192# Job Application Update router 

193job_application_update_router = generate_data_table_crud_router( 

194 table_model=models.JobApplicationUpdate, 

195 create_schema=schemas.JobApplicationUpdateCreate, 

196 update_schema=schemas.JobApplicationUpdateUpdate, 

197 out_schema=schemas.JobApplicationUpdateOut, 

198 endpoint="job-application-updates", 

199 not_found_msg="Job Application Update not found", 

200) 

201 

202# Speculative Application router 

203speculative_application_update_router = generate_data_table_crud_router( 

204 table_model=models.SpeculativeApplication, 

205 create_schema=schemas.SpeculativeApplicationCreate, 

206 update_schema=schemas.SpeculativeApplicationUpdate, 

207 out_schema=schemas.SpeculativeApplicationOut, 

208 endpoint="speculative-applications", 

209 not_found_msg="Speculative Application not found", 

210 many_to_many_fields={ 

211 "contacts": { 

212 "table": models.speculative_application_contact_mapping, 

213 "local_key": "speculative_application_id", 

214 "remote_key": "person_id", 

215 "related_model": models.Person, 

216 }, 

217 }, 

218)