def get(self, request, owners=None, format=None): """ Return sets of parcels grouped by owner """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # TODO get cache working? # cache_key = 'ownership_groups' # cached_content = cache.get(cache_key, None) # if cached_content: # return Response(cached_content) # hardcode the owners, for own - TODO fix this owners = [ 'DETROIT LAND BANK AUTHORITY', 'CITY OF DETROIT-P&DD', 'MI LAND BANK FAST TRACK AUTH', 'HANTZ WOODLANDS LLC', 'TAXPAYER' ] # excecute the search parcels = ParcelMaster.objects.filter(ownername1__in=owners) content = [{ "pnum": parcel.pnum, "address": parcel.propstreetcombined, "owner_name": parcel.ownername1 } for parcel in parcels] # cache.set(cache_key, content, 60 * 60) return Response(content)
def get_escrow_data(request, item_num, param=None): """ Returns escrow data for the given item num. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # Only allow certain servers to call this endpoint if security.block_client(request): # pragma: no cover remote_addr = request.META.get('REMOTE_ADDR') SlackMsgHandler().send_admin_alert( "Address {} was blocked from subscribing waste alerts".format( remote_addr)) return Response("Invalid caller ip or host name: " + remote_addr, status=status.HTTP_403_FORBIDDEN) # Only call via https... if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) balances = EscrowBalance.objects.filter(item_num=item_num) if not balances.exists(): raise Http404("No escrow data found for item number " + str(item_num)) return Response(balances.first().to_json())
def get_auth_token(request): CODLogger.instance().log_api_call(name=__name__, msg=request.path) if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) if not request.body or request.body == b'{}': return Response({"required": ["email", "password"]}, status=status.HTTP_400_BAD_REQUEST) # Parse the data and get email and password data = json.loads(request.body.decode('utf-8')) if not data.get('email') or not data.get('password'): return Response({"required": ["email", "password"]}, status=status.HTTP_400_BAD_REQUEST) email = data['email'] password = data['password'] # Try to authenticate the user # Note: we are using email for username user = authenticate_user(email, password) if not user: return Response({"user not authorized": email}, status=status.HTTP_401_UNAUTHORIZED) # Authentication succeeded: return an auth token token, created = Token.objects.using('photo_survey').get_or_create( user=user) return Response({"token": token.key}, status=status.HTTP_201_CREATED)
def get_sales_property_address(request, address=None, years_back=None, format=None): """ Retrieve property info via address """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # urls with dots are problematic: substitute underscores for dots in the url # (and replace underscores with dots here) address = address.replace('_', '.') # Search for parcels with the given parcel num # pnum = request.path_info.split('/')[2] results = Sales.objects.filter(addresscombined__contains=address) # filter recent-years only? if years_back != None: results = filter_years_back(results, years_back) # if no results found, return 404 if len(results) == 0: raise Http404("Address " + address + " not found") return get_parcels(results)
def delete(self, request, username, parcel_id, format=None): """ Do a soft-delete on a user's favorite. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) parcel_id = get_parcel_id(request.path, 5) users = User.objects.using('photo_survey').filter(username=username) if not users: return Response({"User not found": username}, status=status.HTTP_404_NOT_FOUND) favorites = Survey.objects.filter( survey_type__survey_template_id='bridging_neighborhoods').filter( user_id=users[0].id).filter(parcel__parcel_id=parcel_id) if not favorites: return Response({"favorite not found": parcel_id}, status=status.HTTP_404_NOT_FOUND) for favorite in favorites: favorite.status = 'deleted' favorite.save() return Response(status=status.HTTP_204_NO_CONTENT)
def get_dte_active_connection(request, parcel_id, param=None): """ Returns data cached for the given data source, updating the data whenever necessary. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # Only allow certain servers to call this endpoint if security.block_client(request): # pragma: no cover remote_addr = request.META.get('REMOTE_ADDR') SlackMsgHandler().send_admin_alert( "Address {} was blocked from subscribing waste alerts".format( remote_addr)) return Response("Invalid caller ip or host name: " + remote_addr, status=status.HTTP_403_FORBIDDEN) # Only call via https... if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) parcel_id = get_parcel_id(request.path, 4) if not DTEActiveGasSite.objects.filter(parcel_id=parcel_id).exists(): raise Http404("No parcel " + parcel_id + " found with active connection") return Response({"active": True})
def get_schedule_details(request, waste_area_ids=None, year=None, month=None, format=None): """ List details to the waste collection schedule for a waste area """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # Throw error if there is an unrecognized query param for param in request.query_params.keys(): if param not in ['today']: return Response("Invalid param: " + param, status=status.HTTP_400_BAD_REQUEST) # Allow caller to specify what 'today' is today = request.query_params.get('today') if not today: today = datetime.date.today() if type(today) is str: today = datetime.date(int(today[0:4]), int(today[4:6]), int(today[6:8])) # get details that apply citywide citywide_details = ScheduleDetail.objects.filter(waste_area_ids__exact='') wa_ids = [int(wa_id) for wa_id in waste_area_ids.split(',')] wa_details = ScheduleDetail.objects.none() # get waste schedule details for each waste area requested for wa_id in wa_ids: wa_details = wa_details | ScheduleDetail.objects.filter( waste_area_ids__contains=wa_id) if month or year: citywide_details = util.filter_month(year, month, citywide_details) wa_details = util.filter_month(year, month, wa_details) # sort the different sets of results by 'normal_day' details = sorted(chain(citywide_details, wa_details), key=attrgetter('sort_value')) # get next pickup for each route next_pickups = get_next_pickups(wa_ids, details, today) # build an array of json objects, one for each detail content = { 'next_pickups': next_pickups, 'details': [detail.json() for detail in details] } return Response(content)
def get_sales_property_address_recent(request, address=None, format=None): """ Retrieve property info via address, for recent years only """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) return get_sales_property_address(request._request, address=address, years_back=5, format=format)
def get_sales_property_recent(request, pnum=None, format=None): """ Retrieve property info via parcel id (aka 'pnum'), for recent years only """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) return get_sales_property(request._request, pnum=pnum, years_back=5, format=format)
def get_survey(request, survey_id): """ Get photos and survey data for the given parcel. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) surveys = Survey.objects.filter(id=int(survey_id)).exclude( status='deleted') if not surveys: return Response({"survey not found": survey_id}, status=status.HTTP_404_NOT_FOUND) return Response(get_survey_data(surveys[0]))
def get_survey_count(request, parcel_id): """ Get number of surveys that exist currently for the given parcel. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) parcel_id = get_parcel_id(request.path, 3) count = Survey.objects.filter(parcel__parcel_id=parcel_id).exclude( status='deleted').count() content = {"count": count} return Response(content)
def get_status_summary(request): """ Returns general info: number of parcels that currently have surveys, etc. TODO: add any other metadata here? e.g., add in this, but for residential / commercial: num_parcels_total = ParcelMaster.objects.count() """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) num_parcels_surveyed = Survey.objects.values( 'parcel_id').distinct().count() return Response({"num_parcels_surveyed": num_parcels_surveyed})
def get_image(request, id, format=None): """ Retrieve an image from the assessors database. """ # REVIEW TODO add frank's suggestions CODLogger.instance().log_api_call(name=__name__, msg=request.path) sketches = Sketch.objects.filter(id=int(id)) if not sketches: raise Http404("Sketch id " + id + " not found") sketch = sketches.first() return HttpResponse(sketch.imageData, content_type="image/jpg")
def get_latest_survey(request, parcel_id): """ Returns latest survey data for the parcel. TODO return latest survey that has a 'good' status. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) parcel_id = get_parcel_id(request.path, 4) survey = Survey.objects.filter(parcel__parcel_id=parcel_id).exclude( status='deleted').last() content = get_survey_data(survey) if survey else {} return Response(content)
def get_status(request): """ Returns all parcels that have already been surveyed. Each parcel is paired with the number of times it has been surveyed. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) parcel_counts = ParcelMetadata.objects.all().annotate( count=Count('survey')) content = { parcel_count.parcel_id: parcel_count.count for parcel_count in parcel_counts } return Response(content)
def get_ticket_info(request, ticket_id): """ Returns list of all tickets for a given date. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) table_name = "[COURT36TICKETS].[SWEETSpower].[tblZTickets]" if settings.RUNNING_UNITTESTS: table_name = "tblZTickets" tickets = Tblztickets.objects.raw( "select * from {} where zticketid = {}".format(table_name, ticket_id)) ticket_list = [ticket.zticketid for ticket in tickets] content = {"tickets": ticket_list} return Response(content)
def get_surveyor_survey_count(request): """ Returns number of surveys each surveyor has completed. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) user_info = {user.id: user for user in User.objects.using('photo_survey')} survey_counts = Survey.objects.values('user_id').annotate( count=Count('parcel_id', distinct=True)).order_by('-count') results = [] for survey_count in survey_counts: user = user_info[int(survey_count['user_id'])] results.append({user.email: survey_count['count']}) return Response(results)
def confirm(request, client_id): """ Parse subscription confirmation and send a simple response. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) client = get_existing_object(cl_type=MessengerClient, obj_id=client_id, cl_name="Client", required=True) msg_handler = get_messenger_msg_handler(client) # Make sure the call came from twilio and is valid msg_handler.validate(request) # Verify required fields are present if not request.data.get('From') or not request.data.get('Body'): return Response({"error": "From and body values are required"}, status=status.HTTP_400_BAD_REQUEST) # Clean up phone number phone_number = MsgHandler.get_fone_number(request) # Retrieve the subscriber subscribers = MessengerSubscriber.objects.filter(phone_number=phone_number, messenger_clients=client) if not subscribers.exists(): message = "Subscriber for phone_number {phone_number} for client {client_name} not found".format( phone_number=phone_number, client_name=client.name) raise NotFound(detail={"error": message}) subscriber = subscribers.get() # Update the subscriber's status subscriber.change_status(activate=True) # Let the subscriber know their notifications were activated. # REVIEW: make message configurable? msg_handler.send_text(phone_number=phone_number, text="Your {} alerts have been activated".format( client.name)) return Response({"subscriber": str(subscriber), "message": phone_number})
def get_parcel(request, pnum=None, format=None): """ Return parcel data from the assessors dataset """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # clean up the pnum pnum = get_parcel_id(request.path, 3) # excecute the search parcels = BSAPARCELDATA.objects.filter(PARCELNO=pnum) if not parcels: raise Http404("Parcel id " + pnum + " not found") content = parcels[0].to_json() content['field_descriptions'] = util.get_parcel_descriptions() return Response(content)
def get_rental_cases(request, pnum=None, format=None): """ Return rental unit cases from tidemark (oracle) """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # TODO need massive cleanup for tidemark pnums # clean up the pnum # pnum = util.clean_pnum(pnum) # excecute the search casemains = CaseMain.objects.filter( prc_parcel_no__prc_parcel_no__exact=pnum) if not casemains.exists(): raise Http404("Parcel id " + pnum + " not found") content = [casemain.to_json() for casemain in casemains] return Response(content)
def get_images(request, pnum=None, format=None): """ Retrieve all images for the given parcel. """ # REVIEW TODO add frank's suggestions CODLogger.instance().log_api_call(name=__name__, msg=request.path) # clean up the pnum pnum = get_parcel_id(request.path, 2) sketch_data = Sketch.objects.filter(pnum=pnum).order_by('-date') content = [] # REVIEW (TODO) remove the call to move_files() ... for sketch in sketch_data: content.append(sketch.to_json()) return Response(content)
def subscribe_web(request, client_id): """ Parse subscription request and text user request for confirmation. { "phone_number": "2124831691", "address": "27 Montclair Rd", "lang": "en|es|ar|bn" (optional - default is "en") } """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) client = get_existing_object(cl_type=MessengerClient, obj_id=client_id, cl_name="Client", required=True) msg_handler = get_messenger_msg_handler(client) # Verify required fields are present if not request.data.get('phone_number') or not request.data.get('address'): return Response({"error": "Address and phone number are required"}, status=status.HTTP_400_BAD_REQUEST) # Clean up phone number phone_number_from = MsgHandler.get_fone_number(request, key='phone_number') # Clean up street address street_address = MsgHandler.get_address(request=request, key='address') lang = request.data.get("lang", "en") return subscriber_helper(phone_number_from=phone_number_from, msg_handler=msg_handler, client=client, street_address=street_address, lang=lang, text_signup=False)
def get_notifications(request, client_id=None): """ Returns all notifications for a client. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) if client_id: return Response(get_notifications_helper(client_id=client_id)) else: response = [] for client in MessengerClient.objects.all(): response.append( get_notifications_helper(client_id=client.id, client_only=True)) return Response(response)
def add_notification_message(request, notification_id, message_id=None): """ Adds or updates a message for a notification. { "lang": "es", "message": "<insert message here>" } """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) notification = get_existing_object(cl_type=MessengerNotification, obj_id=notification_id, cl_name="Notification", required=True) messenger_message = get_existing_object(cl_type=MessengerMessage, obj_id=message_id, cl_name="Message") if not request.data.get('lang') or not request.data.get('message'): return Response({"error": "lang and message are required"}, status=status.HTTP_400_BAD_REQUEST) lang = request.data['lang'] message = request.data['message'] if not messenger_message: messenger_message = MessengerMessage( messenger_notification=notification, lang=lang, message=message) else: messenger_message.lang = lang messenger_message.message = message messenger_message.save() return Response(messenger_message.to_json(), status=status.HTTP_201_CREATED)
def get_sales_property(request, pnum=None, years_back=None, format=None): """ Retrieve property info via parcel id (aka 'pnum') """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) pnum = get_parcel_id(request.path, 2) # Search for parcels with the given parcel num # pnum = request.path_info.split('/')[2] results = Sales.objects.filter(pnum__iexact=pnum) # filter recent-years only? if years_back != None: results = filter_years_back(results, years_back) # if no results found, return 404 if len(results) == 0: raise Http404("Parcel id " + pnum + " not found") return get_parcels(results)
def get_metadata(request, parcel_id): """ Get photos and survey data for the given parcel """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) parcel_id = get_parcel_id(request.path, 2) # TODO possibly optimize the images on the disk? # https://hugogiraudel.com/2013/07/29/optimizing-with-bash/ # e.g., jpegtran -progressive image_raw.jpg small.jpg images = [] image_metadata = ImageMetadata.objects.filter(parcel__parcel_id=parcel_id) for img_meta in image_metadata: url = request.build_absolute_uri( location='/data/photo_survey/images/' + img_meta.image.file_path) images.append(url) surveys = [ survey.id for survey in Survey.objects.filter( parcel__parcel_id=parcel_id).exclude(status='deleted') ] # Is this parcel publicly owned? # TODO: make sure the dataset for this eventually 'goes live' (currently we load it # whenever dexter slusarski gets a new csv with this content) public_property_data = PublicPropertyData.objects.filter( parcelno=parcel_id) publicly_owned = len(public_property_data) > 0 return Response({ "images": images, "surveys": surveys, "publicly_owned": publicly_owned })
def get(self, request, username, format=None): """ Returns list of parcels (houses) a resident is interested in. """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) users = User.objects.using('photo_survey').filter(username=username) if not users: return Response({"User not found": username}, status=status.HTTP_404_NOT_FOUND) surveys = Survey.objects.filter(user_id=users[0].id).exclude( status='deleted') favorites = { survey.parcel.parcel_id: self.describe_favorite(survey) for survey in surveys } return Response({"favorites": favorites})
def subscribe(request): """ Parse subscription request and text user request for confirmation. { 'From': [ '5005550007' ], 'Body': [ '7840 van dyke pl' ], 'To': [ '5005550006' ] } """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) # # Only allow certain servers to call this endpoint # if cod_utils.security.block_client(request): # remote_addr = request.META.get('REMOTE_ADDR') # SlackMsgHandler().send_admin_alert("Address {} was blocked from subscribing waste alerts".format(remote_addr)) # return Response("Invalid caller ip or host name: " + remote_addr, status=status.HTTP_403_FORBIDDEN) # Verify required fields are present if not request.data.get('From') or not request.data.get('Body'): return Response({"error": "Address and phone_number are required"}, status=status.HTTP_400_BAD_REQUEST) # Clean up phone numbers phone_number_from = MsgHandler.get_fone_number(request, key='From') phone_number_to = MsgHandler.get_fone_number(request, key='To') # Make sure phone number is set up and valid. if not MessengerPhoneNumber.objects.filter( phone_number=phone_number_to).exists(): return Response( { "error": "phone_number {phone_number_to} not found".format( phone_number_to=phone_number_to) }, status=status.HTTP_404_NOT_FOUND) # Figure out what messenger client this is and get the msg handler for this client. phone_number_to_object = MessengerPhoneNumber.objects.get( phone_number=phone_number_to) client = phone_number_to_object.messenger_client msg_handler = get_messenger_msg_handler(client) # Make sure the call came from twilio and is valid # Note: would be nice to validate earlier but we haven't yet been # able to construct our msg_handler until now... msg_handler.validate(request) # Clean up street address street_address = MsgHandler.get_address(request=request) return subscriber_helper(phone_number_from=phone_number_from, msg_handler=msg_handler, client=client, street_address=street_address, lang='en', text_signup=True)
def update_or_create_from_dict(data): """ Using dictionary of form data, update existing subscriber or create new one """ if not data.get('phone_number') or not data.get('address'): return None, {"error": "address and phone_number are required"} phone_number = data['phone_number'] street_address = data['address'] if not re.fullmatch(r'[\d]{10}', phone_number): return None, { "error": "phone_number must be 9 digits, no punctuation" } waste_area_ids = data.get('waste_area_ids') if not waste_area_ids: # Parse address string and get result from AddressPoint geocoder location, address = util.geocode_address( street_address=street_address) if not location: invalid_addr_msg = 'Invalid waste reminder text signup: {} from {}'.format( street_address, phone_number) CODLogger.instance().log_error( name=__name__, area="waste notifier signup by text", msg=invalid_addr_msg) SlackMsgHandler().send_admin_alert(invalid_addr_msg) msg = "Unfortunately, address {} could not be located - please text the street address only, for example '1301 3rd ave'".format( street_address) text_signup_number = settings.AUTO_LOADED_DATA[ "WASTE_REMINDER_TEXT_SIGNUP_NUMBERS"][0] get_dpw_msg_handler().send_text( phone_number=phone_number, phone_sender=text_signup_number, text=msg) return None, {"error": "Address not found"} waste_area_ids = get_waste_area_ids(location=location) if type(waste_area_ids) == list: waste_area_ids = ''.join( [str(num) + ',' for num in waste_area_ids]) # update existing subscriber or create new one subscriber = Subscriber.objects.none() previous = Subscriber.objects.filter(phone_number__exact=phone_number) if previous.exists(): subscriber = previous[0] subscriber.phone_number = phone_number subscriber.waste_area_ids = waste_area_ids else: # try to create a subscriber with the posted data subscriber = Subscriber(phone_number=phone_number, waste_area_ids=waste_area_ids) # set service type if data.get("service_type"): subscriber.service_type = data['service_type'].replace('|', ',') # check for optional values for value in ['address', 'latitude', 'longitude']: if data.get(value): setattr(subscriber, value, data.get(value)) # validate and save subscriber subscriber.clean() subscriber.save() return subscriber, None
def post(self, request, parcel_id, format=None): """ Post results of a field survey """ CODLogger.instance().log_api_call(name=__name__, msg=request.path) if not request.is_secure(): return Response({"error": "must be secure"}, status=status.HTTP_403_FORBIDDEN) # TODO remove this once we get mod_wsgi passing the authorization header through properly auth_meta = request.META.get('HTTP_AUTHORIZATION') if auth_meta: print(auth_meta) else: print('saw no http auth meta') data = json.loads(request.body.decode('utf-8')) # What user is posting this? user = self.get_surveyor(request, data) if user == None: return Response({"error": "user not authorized"}, status=status.HTTP_401_UNAUTHORIZED) survey_template_id = self.get_survey_template_id(data) parcel_id = get_parcel_id(request.path, self.get_parcel_id_offset()) answer_errors = {} # Is the parcel id valid? if not ParcelMaster.objects.filter(pnum__iexact=parcel_id).exists(): return Response({"invalid parcel id": parcel_id}, status=status.HTTP_400_BAD_REQUEST) # What are our questions and answers? questions = SurveyQuestion.objects.filter( survey_type__survey_template_id=survey_template_id).order_by( 'question_number') if not questions: return Response({"invalid survey": data['survey_id']}, status=status.HTTP_400_BAD_REQUEST) answers = {answer['question_id']: answer for answer in data['answers']} # Report any answers that did not match a question_id question_ids = {question.question_id for question in questions} orphaned_answers = { key for key in answers.keys() if key not in question_ids } if orphaned_answers: return Response({"invalid question ids": list(orphaned_answers)}, status=status.HTTP_400_BAD_REQUEST) # Validate each answer for question in questions: answer = answers.get(question.question_id) if answer and answer.get('answer'): if not question.is_valid(answer['answer']): answer_errors[ question.question_id] = "question answer is invalid" elif question.answer_trigger and question.answer_trigger_action: # TODO clean this up - add 'skip to question' feature if re.fullmatch( question.answer_trigger, answer['answer'] ) and question.answer_trigger_action == 'exit': break elif SurveyorView.is_answer_required(question, answers): answer_errors[ question.question_id] = "question answer is required" # Report invalid content? if answer_errors: return Response(answer_errors, status=status.HTTP_400_BAD_REQUEST) parcel, created = ParcelMetadata.objects.get_or_create( parcel_id=parcel_id) # Create the survey survey_type = SurveyType.objects.get( survey_template_id=survey_template_id) survey = Survey(survey_type=survey_type, user_id=str(user.id), parcel=parcel, common_name=data.get('common_name', ''), note=data.get('note', ''), status=data.get('status', ''), image_url=data.get('image_url', '')) survey.save() # Save all the answers for survey_question in questions: answer = answers.get(survey_question.question_id) if answer and answer['answer']: survey_answer = SurveyAnswer(survey=survey, survey_question=survey_question, answer=answer['answer'], note=answer.get('answer', None)) survey_answer.save() # Indicate number of surveys present for each parcel id in the request parcel_info = self.check_parcels(data.get('parcel_ids', [])) return Response({ "answers": answers, "parcel_survey_info": parcel_info }, status=status.HTTP_201_CREATED)