Skip to content

Commit 21bf703

Browse files
authored
Merge pull request #338 from tutorcruncher/contractor-mass
Mass Create Contractors
2 parents f97c29a + 20e1760 commit 21bf703

19 files changed

+602
-106
lines changed

tcsocket/app/main.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@
2020
service_list,
2121
)
2222
from .views.company import company_create, company_list, company_options, company_update
23-
from .views.contractor import contractor_get, contractor_list, contractor_set
23+
from .views.contractor import contractor_get, contractor_list, contractor_set, contractor_set_mass
2424
from .views.enquiry import clear_enquiry, enquiry
2525

2626

2727
async def startup(app: web.Application):
2828
settings: Settings = app['settings']
2929
redis = await create_pool(settings.redis_settings)
3030
app.update(
31-
pg_engine=await create_engine(settings.pg_dsn), redis=redis, session=ClientSession(),
31+
pg_engine=await create_engine(settings.pg_dsn),
32+
redis=redis,
33+
session=ClientSession(),
3234
)
3335

3436

@@ -52,6 +54,7 @@ def setup_routes(app):
5254
# to work with tutorcruncher websockets
5355
app.router.add_post(r'/{company}/webhook/options', company_update, name='company-update')
5456
app.router.add_post(r'/{company}/webhook/contractor', contractor_set, name='webhook-contractor')
57+
app.router.add_post(r'/{company}/webhook/contractor/mass', contractor_set_mass, name='webhook-contractor-mass')
5558
app.router.add_post(r'/{company}/webhook/clear-enquiry', clear_enquiry, name='webhook-clear-enquiry')
5659
app.router.add_post(r'/{company}/webhook/appointments/{id:\d+}', appointment_webhook, name='webhook-appointment')
5760
app.router.add_post(

tcsocket/app/middleware.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ async def company_middleware(request, handler):
164164
request['company'] = company
165165
else:
166166
raise HTTPNotFoundJson(
167-
status='company not found', details=f'No company found for key {public_key}',
167+
status='company not found',
168+
details=f'No company found for key {public_key}',
168169
)
169170
return await handler(request)
170171
except CancelledError:
@@ -190,7 +191,8 @@ async def json_request_middleware(request, handler):
190191

191192
if error_details:
192193
raise HTTPBadRequestJson(
193-
status='invalid request data', details=error_details,
194+
status='invalid request data',
195+
details=error_details,
194196
)
195197
return await handler(request)
196198

@@ -202,7 +204,8 @@ def _check_timestamp(ts: str, now):
202204
raise ValueError()
203205
except (TypeError, ValueError):
204206
raise HTTPForbiddenJson(
205-
status='invalid request time', details=f"request time '{ts}' not in the last 10 seconds",
207+
status='invalid request time',
208+
details=f"request time '{ts}' not in the last 10 seconds",
206209
)
207210

208211

@@ -221,7 +224,8 @@ async def authenticate(request, api_key=None):
221224
if _api_key and signature == hmac.new(_api_key, body, hashlib.sha256).hexdigest():
222225
return
223226
raise HTTPUnauthorizedJson(
224-
status='invalid signature', details=f'Signature header "{signature}" does not match computed signature',
227+
status='invalid signature',
228+
details=f'Signature header "{signature}" does not match computed signature',
225229
)
226230

227231

tcsocket/app/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ class Contractor(Base):
4444
id = Column(Integer, primary_key=True, autoincrement=False, nullable=False)
4545
company = Column(Integer, ForeignKey('companies.id'), nullable=False)
4646

47-
first_name = Column(String(63), index=True)
48-
last_name = Column(String(63))
47+
first_name = Column(String(255), index=True)
48+
last_name = Column(String(255))
4949

5050
town = Column(String(63))
5151
country = Column(String(63))

tcsocket/app/processing.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ def _get_special_extra_attr(extra_attributes: List[ExtraAttributeModel], machine
108108

109109

110110
async def contractor_set(
111-
*, conn, company, contractor: ContractorModel, skip_deleted=False, redis=None, ctx=None,
111+
*,
112+
conn,
113+
company,
114+
contractor: ContractorModel,
115+
process_profile_pic=True,
116+
skip_deleted=False,
117+
redis=None,
118+
ctx=None,
112119
) -> Action:
113120
"""
114121
Create or update a contractor.
@@ -119,6 +126,7 @@ async def contractor_set(
119126
:param company: dict with company info, including id and public_key
120127
:param contractor: data about contractor
121128
:param skip_deleted: whether or not to skip deleted contractors (or delete them in the db.)
129+
:param process_profile_pic: whether or not to run process_image
122130
:return: Action: created, updated or deleted
123131
"""
124132
from .worker import process_image
@@ -132,7 +140,8 @@ async def contractor_set(
132140
)
133141
if not await curr.first():
134142
raise HTTPNotFoundJson(
135-
status='not found', details=f'contractor with id {contractor.id} not found',
143+
status='not found',
144+
details=f'contractor with id {contractor.id} not found',
136145
)
137146
return Action.deleted
138147

@@ -174,11 +183,12 @@ async def contractor_set(
174183
if r is None:
175184
# the contractor already exists but on another company
176185
raise HTTPForbiddenJson(
177-
status='permission denied', details=f'you do not have permission to update contractor {contractor.id}',
186+
status='permission denied',
187+
details=f'you do not have permission to update contractor {contractor.id}',
178188
)
179189
await _set_skills(conn, contractor.id, contractor.skills)
180190
await _set_labels(conn, company['id'], contractor.labels)
181-
if contractor.photo:
191+
if process_profile_pic and contractor.photo:
182192
# Sometimes creating the contractor is already done on a job, so don't need another one.
183193
job_kwargs = dict(company_key=company['public_key'], contractor_id=contractor.id, url=contractor.photo)
184194
if redis:

tcsocket/app/settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ class Settings(BaseSettings):
3333
def parse_redis_settings(cls, v):
3434
conf = urlparse(v)
3535
return RedisSettings(
36-
host=conf.hostname, port=conf.port, password=conf.password, database=int((conf.path or '0').strip('/')),
36+
host=conf.hostname,
37+
port=conf.port,
38+
password=conf.password,
39+
database=int((conf.path or '0').strip('/')),
3740
)
3841

3942
@property

tcsocket/app/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def pretty_lenient_json(data):
5151
class HTTPClientErrorJson(web.HTTPClientError):
5252
def __init__(self, **data):
5353
super().__init__(
54-
text=pretty_lenient_json(data), content_type=JSON_CONTENT_TYPE, headers=ACCESS_CONTROL_HEADERS,
54+
text=pretty_lenient_json(data),
55+
content_type=JSON_CONTENT_TYPE,
56+
headers=ACCESS_CONTROL_HEADERS,
5557
)
5658

5759

@@ -109,7 +111,8 @@ def get_arg(request, field, *, decoder: Callable[[str], Any] = int, default: Any
109111
return None if v is None else decoder(v)
110112
except ValueError:
111113
raise HTTPBadRequestJson(
112-
status='invalid_argument', details=f'"{field}" had an invalid value "{v}"',
114+
status='invalid_argument',
115+
details=f'"{field}" had an invalid value "{v}"',
113116
)
114117

115118

tcsocket/app/validation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ class EATypeEnum(str, Enum):
132132
class ContractorModel(BaseModel):
133133
id: int
134134
deleted: bool = False
135-
first_name: constr(max_length=63) = None
136-
last_name: constr(max_length=63) = None
135+
first_name: constr(max_length=255) = None
136+
last_name: constr(max_length=255) = None
137137
town: constr(max_length=63) = None
138138
country: constr(max_length=63) = None
139139
last_updated: datetime = None
@@ -189,6 +189,7 @@ def val_upstream_http_referrer(cls, v):
189189

190190

191191
class AppointmentModel(BaseModel):
192+
id: int
192193
service_id: int
193194
service_name: str
194195
extra_attributes: List[ExtraAttributeModel]

tcsocket/app/views/appointments.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ async def appointment_webhook(request):
5555
pg_insert(sa_services)
5656
.values(id=appointment.service_id, company=request['company'].id, **service_insert_update)
5757
.on_conflict_do_update(
58-
index_elements=[ser_c.id], where=ser_c.id == appointment.service_id, set_=service_insert_update,
58+
index_elements=[ser_c.id],
59+
where=ser_c.id == appointment.service_id,
60+
set_=service_insert_update,
5961
)
6062
)
6163
apt_insert_update = appointment.dict(
@@ -66,17 +68,19 @@ async def appointment_webhook(request):
6668
await conn.execute(
6769
pg_insert(sa_appointments)
6870
.values(id=apt_id, service=appointment.service_id, **apt_insert_update)
69-
.on_conflict_do_update(index_elements=[apt_c.id], where=apt_c.id == apt_id, set_=apt_insert_update,)
71+
.on_conflict_do_update(
72+
index_elements=[apt_c.id],
73+
where=apt_c.id == apt_id,
74+
set_=apt_insert_update,
75+
)
7076
)
7177
return json_response(request, status='success')
7278

7379

7480
async def appointment_webhook_mass(request):
7581
conn = await request['conn_manager'].get_connection()
7682
data = await request.json()
77-
if data['_request_time']:
78-
del data['_request_time']
79-
for apt_id, apt in data.items():
83+
for apt in data['appointments']:
8084
if apt['ss_method'] == 'POST':
8185
appointment = AppointmentModel(**apt)
8286

@@ -86,7 +90,7 @@ async def appointment_webhook_mass(request):
8690
raise HTTPConflictJson(
8791
status='service conflict',
8892
details=f'service {appointment.service_id} already exists'
89-
' and is associated with another company',
93+
' and is associated with another company',
9094
)
9195

9296
service_insert_update = dict(
@@ -102,7 +106,9 @@ async def appointment_webhook_mass(request):
102106
pg_insert(sa_services)
103107
.values(id=appointment.service_id, company=request['company'].id, **service_insert_update)
104108
.on_conflict_do_update(
105-
index_elements=[ser_c.id], where=ser_c.id == appointment.service_id, set_=service_insert_update,
109+
index_elements=[ser_c.id],
110+
where=ser_c.id == appointment.service_id,
111+
set_=service_insert_update,
106112
)
107113
)
108114
apt_insert_keys = [
@@ -120,12 +126,16 @@ async def appointment_webhook_mass(request):
120126

121127
await conn.execute(
122128
pg_insert(sa_appointments)
123-
.values(id=apt_id, service=appointment.service_id, **apt_insert_update)
124-
.on_conflict_do_update(index_elements=[apt_c.id], where=apt_c.id == apt_id, set_=apt_insert_update,)
129+
.values(id=appointment.id, service=appointment.service_id, **apt_insert_update)
130+
.on_conflict_do_update(
131+
index_elements=[apt_c.id],
132+
where=apt_c.id == appointment.id,
133+
set_=apt_insert_update,
134+
)
125135
)
126136
elif apt['ss_method'] == 'DELETE':
127137
await conn.execute(
128-
sa_appointments.delete().where(and_(apt_c.id == apt_id, ser_c.company == request['company'].id))
138+
sa_appointments.delete().where(and_(apt_c.id == apt['id'], ser_c.company == request['company'].id))
129139
)
130140
else:
131141
return
@@ -208,7 +218,11 @@ async def appointment_list(request):
208218
q_count = select([sql_f.count()]).select_from(sa_appointments.join(sa_services)).where(and_(*where))
209219
cur_count = await conn.execute(q_count)
210220

211-
return json_response(request, results=results, count=(await cur_count.first())[0],)
221+
return json_response(
222+
request,
223+
results=results,
224+
count=(await cur_count.first())[0],
225+
)
212226

213227

214228
async def service_list(request):
@@ -240,7 +254,11 @@ async def service_list(request):
240254
select([sql_f.count(distinct(ser_c.id))]).select_from(sa_appointments.join(sa_services)).where(and_(*where))
241255
)
242256

243-
return json_response(request, results=results, count=(await cur_count.first())[0],)
257+
return json_response(
258+
request,
259+
results=results,
260+
count=(await cur_count.first())[0],
261+
)
244262

245263

246264
class SSOData(BaseModel):
@@ -273,7 +291,8 @@ def _get_sso_data(request, company) -> SSOData:
273291
sso_data: SSOData = SSOData.parse_raw(sso_data_, proto=Protocol.json)
274292
except ValidationError as e:
275293
raise HTTPBadRequestJson(
276-
status='invalid request data', details=e.errors(),
294+
status='invalid request data',
295+
details=e.errors(),
277296
)
278297
else:
279298
if sso_data.expires < datetime.astimezone(datetime.now(), timezone.utc):
@@ -318,7 +337,13 @@ async def book_appointment(request):
318337
v = await conn.execute(
319338
select([apt_c.attendees_current_ids])
320339
.select_from(sa_appointments.join(sa_services))
321-
.where(and_(ser_c.company == company.id, apt_c.start > datetime.utcnow(), apt_c.id == booking.appointment,))
340+
.where(
341+
and_(
342+
ser_c.company == company.id,
343+
apt_c.start > datetime.utcnow(),
344+
apt_c.id == booking.appointment,
345+
)
346+
)
322347
)
323348
r = await v.first()
324349
if not r:

tcsocket/app/views/company.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ async def company_create(request):
1616
1717
Authentication and json parsing are done by middleware.
1818
"""
19+
data = await request.json()
20+
update_contractors = data.pop('update_contractors', True)
1921
company: CompanyCreateModal = request['model']
2022
existing_company = bool(company.private_key)
2123
data = company.dict()
@@ -30,7 +32,8 @@ async def company_create(request):
3032
new_company = await v.first()
3133
if new_company is None:
3234
raise HTTPConflictJson(
33-
status='duplicate', details='the supplied data conflicts with an existing company',
35+
status='duplicate',
36+
details='the supplied data conflicts with an existing company',
3437
)
3538
else:
3639
logger.info(
@@ -40,7 +43,7 @@ async def company_create(request):
4043
new_company.public_key,
4144
new_company.private_key,
4245
)
43-
if existing_company:
46+
if update_contractors and existing_company:
4447
await request.app['redis'].enqueue_job('update_contractors', company=dict(new_company))
4548
return json_response(
4649
request,
@@ -74,6 +77,8 @@ async def company_update(request):
7477
"""
7578
Modify a company.
7679
"""
80+
data = await request.json()
81+
update_contractors = data.pop('update_contractors', True)
7782
company: CompanyUpdateModel = request['model']
7883
data = company.dict(include={'name', 'public_key', 'private_key', 'name_display'})
7984
data = {k: v for k, v in data.items() if v is not None}
@@ -97,10 +102,17 @@ async def company_update(request):
97102
select_fields = c.id, c.public_key, c.private_key, c.name_display, c.domains
98103
q = select(select_fields).where(c.public_key == public_key)
99104
result = await conn.execute(q)
100-
company = dict(await result.first())
101-
102-
await request.app['redis'].enqueue_job('update_contractors', company=company)
103-
return json_response(request, status_=200, status='success', details=data, company_domains=company['domains'],)
105+
company: dict = dict(await result.first())
106+
107+
if update_contractors:
108+
await request.app['redis'].enqueue_job('update_contractors', company=company)
109+
return json_response(
110+
request,
111+
status_=200,
112+
status='success',
113+
details=data,
114+
company_domains=company['domains'],
115+
)
104116

105117

106118
async def company_list(request):

0 commit comments

Comments
 (0)