Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provisioning #1463

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c82eaed
upload profile image
jefer94 Sep 17, 2024
e0efb84
test profile upload and notification emission
jefer94 Sep 18, 2024
a8ac365
add admin
jefer94 Sep 18, 2024
7a989f6
Create base_provisioning.html
JennGuz Sep 19, 2024
05cb123
Update choose_vendor.html
JennGuz Sep 19, 2024
093dc10
Add files via upload
JennGuz Sep 19, 2024
7023ede
Update deal_update.py
alesanchezr Sep 24, 2024
f993415
added application json and text/plain as allowed mime
alesanchezr Sep 24, 2024
4cc7b1c
Merge pull request #1461 from jefer94/feat/upload-profile-image
tommygonzaleza Sep 24, 2024
ba19a1a
update formentry.status to include MANUALLY_PERSISTED
alesanchezr Sep 24, 2024
ccaaab8
update formentry.status to include MANUALLY_PERSISTED
alesanchezr Sep 24, 2024
2e70d01
update formentry.status to include MANUALLY_PERSISTED
alesanchezr Sep 24, 2024
bd70440
update formentry.status to include MANUALLY_PERSISTED
alesanchezr Sep 24, 2024
c5c69d2
update formentry.status to include MANUALLY_PERSISTED
alesanchezr Sep 24, 2024
f48fe81
add agent from config to serializer and action
gustavomm19 Sep 25, 2024
a243a44
added 4geeks-com as possible admin option inbulk for formentries
alesanchezr Sep 25, 2024
9f18630
add netcat and root password
jefer94 Sep 25, 2024
7888cb2
add dep
jefer94 Sep 25, 2024
1399bb5
added learnpack_deply_url to the asset
alesanchezr Sep 25, 2024
7ae330e
Merge pull request #1464 from gustavomm19/asset-agent
jefer94 Sep 25, 2024
1cddeba
Merge pull request #1465 from breatheco-de/main
tommygonzaleza Sep 25, 2024
3653d7d
fix migrations conflict
gustavomm19 Sep 26, 2024
6375a49
move agent to another serializer
gustavomm19 Sep 26, 2024
634fa67
Merge pull request #1466 from gustavomm19/asset-agent
jefer94 Sep 26, 2024
91cabba
now brevo is supported in the formentry
alesanchezr Sep 27, 2024
0344ae8
Merge pull request #1467 from breatheco-de/development
tommygonzaleza Sep 27, 2024
1b7fd6d
fixed issue with value_list on formentry
alesanchezr Sep 28, 2024
d3b562f
Merge branch 'main' of https://github.com/breatheco-de/apiv2
alesanchezr Sep 28, 2024
04a4df4
Update serializers.py
tommygonzaleza Sep 30, 2024
d1ec5a9
Merge branch 'breatheco-de:main' into provisioning
JennGuz Oct 1, 2024
0e2498c
fix coupon error when it has 100% discount
jefer94 Oct 1, 2024
4718758
Merge branch 'breatheco-de:main' into provisioning
JennGuz Oct 1, 2024
c0b3986
choose_vendor was changed and a modal was added
JennGuz Oct 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ FACEBOOK_REDIRECT_URL=
ACTIVE_CAMPAIGN_KEY=
ACTIVE_CAMPAIGN_URL=

BREVO_KEY=

GOOGLE_APPLICATION_CREDENTIALS=
GOOGLE_SERVICE_KEY=
GOOGLE_CLOUD_KEY=
Expand Down
4 changes: 3 additions & 1 deletion .gitpod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release
wget --quiet -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - && \
echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" | sudo tee /etc/apt/sources.list.d/llvm.list && \
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - && \
sudo install-packages postgresql-16 postgresql-contrib-16 redis-server
sudo install-packages postgresql-16 postgresql-contrib-16 redis-server netcat passwd

# Setup PostgreSQL server for user gitpod
ENV PATH="/usr/lib/postgresql/16/bin:$PATH"
Expand All @@ -32,6 +32,8 @@ COPY --chown=gitpod:gitpod postgresql-hook.bash $HOME/.bashrc.d/200-postgresql-l
# RUN pyenv install 3.12.3 && pyenv global 3.12.3
# RUN pip install pipenv

RUN echo "root:1234" | chpasswd

USER gitpod

RUN if ! grep -q "export PIP_USER=no" "$HOME/.bashrc"; then printf '%s\n' "export PIP_USER=no" >> "$HOME/.bashrc"; fi
Expand Down
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ zstandard = "*"
psycopg = {extras = ["pool", "binary"] }
cryptography = "*"
adrf = "*"
uvicorn = "*"
django-minify-html = "*"
django-storages = {extras = ["google"] }
aiohttp = {extras = ["speedups"] }
Expand All @@ -149,3 +148,5 @@ capy-core = {extras = ["django"], version = "*"}
google-api-python-client = "*"
python-dotenv = "*"
uvicorn-worker = "*"
python-magic = "*"
uvicorn = {extras = ["standard"], version = "*"}
745 changes: 521 additions & 224 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion breathecode/admissions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ def validate(self, data: OrderedDict):
mandatory_slugs.append(assignment["slug"])

has_tasks = (
Task.objects.filter(associated_slug__in=mandatory_slugs)
Task.objects.filter(associated_slug__in=mandatory_slugs, user_id=user.id, cohort__id=cohort.id)
.exclude(revision_status__in=["APPROVED", "IGNORED"])
.count()
)
Expand Down
14 changes: 12 additions & 2 deletions breathecode/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""

# the rest of your ASGI file contents go here
import os

from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "breathecode.settings")

application = get_asgi_application()
http_application = get_asgi_application()

from .websocket.router import routes

application = ProtocolTypeRouter(
{
"http": http_application,
"websocket": routes,
# Just HTTP for now. (We can add other protocols later.)
}
)
4 changes: 3 additions & 1 deletion breathecode/assignments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from adrf.views import APIView
from asgiref.sync import sync_to_async
from capyc.rest_framework.exceptions import ValidationException
from circuitbreaker import CircuitBreakerError
from django.contrib import messages
from django.db.models import Q
Expand Down Expand Up @@ -31,7 +32,6 @@
from breathecode.utils.decorators.capable_of import acapable_of
from breathecode.utils.i18n import translation
from breathecode.utils.multi_status_response import MultiStatusResponse
from capyc.rest_framework.exceptions import ValidationException

from .actions import deliver_task, sync_cohort_tasks
from .caches import TaskCache
Expand Down Expand Up @@ -62,6 +62,8 @@
"application/pdf",
"image/jpg",
"application/octet-stream",
"application/json",
"text/plain",
]

IMAGES_MIME_ALLOW = ["image/png", "image/svg+xml", "image/jpeg", "image/jpg"]
Expand Down
164 changes: 112 additions & 52 deletions breathecode/marketing/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import requests
from capyc.rest_framework.exceptions import ValidationException
from django.db.models import Q
from django.utils import timezone
from rest_framework.exceptions import APIException
Expand All @@ -14,9 +15,9 @@
from breathecode.authenticate.models import CredentialsFacebook
from breathecode.notify.actions import send_email_message
from breathecode.services.activecampaign import ACOldClient, ActiveCampaign, ActiveCampaignClient, acp_ids, map_ids
from breathecode.services.brevo import Brevo
from breathecode.utils import getLogger
from breathecode.utils.i18n import translation
from capyc.rest_framework.exceptions import ValidationException

from .models import AcademyAlias, ActiveCampaignAcademy, Automation, FormEntry, Tag

Expand Down Expand Up @@ -152,12 +153,16 @@ def validate_email(email, lang):
return email_status


def set_optional(contact, key, data, custom_key=None):
def set_optional(contact, key, data, custom_key=None, crm_vendor="ACTIVE_CAMPAIGN"):
if custom_key is None:
custom_key = key

if custom_key in data:
contact["field[" + acp_ids[key] + ",0]"] = data[custom_key]
if crm_vendor == "ACTIVE_CAMPAIGN":
if custom_key in data:
contact["field[" + acp_ids[key] + ",0]"] = data[custom_key]
else:
if custom_key in data:
contact[key] = data[custom_key]

return contact

Expand All @@ -177,7 +182,7 @@ def get_lead_tags(ac_academy, form_entry):

tags = list(chain(strong_tags, soft_tags, dicovery_tags, other_tags))
if len(tags) != len(_tags):
message = "Some tag applied to the contact not found or have tag_type different than [STRONG, SOFT, DISCOVER, OTHER]: "
message = f"Some tag applied to the contact not found or have tag_type different than [STRONG, SOFT, DISCOVER, OTHER] for this academy {ac_academy.academy.name}. "
message += f'Check for the follow tags: {",".join(_tags)}'
raise Exception(message)

Expand All @@ -198,7 +203,7 @@ def get_lead_automations(ac_academy, form_entry):
raise Exception(f"The specified automation {_name} was not found for this AC Academy")

logger.debug(f"found {str(count)} automations")
return automations.values_list("acp_id", flat=True)
return automations


def add_to_active_campaign(contact, academy_id: int, automation_id: int):
Expand Down Expand Up @@ -250,7 +255,7 @@ def add_to_active_campaign(contact, academy_id: int, automation_id: int):
logger.error(f"error triggering automation with id {str(acp_id)}", response)
raise APIException("Could not add contact to Automation")

logger.info(f"Triggered automation with id {str(acp_id)}", response)
logger.debug(f"Triggered automation with id {str(acp_id)}", response)


def register_new_lead(form_entry=None):
Expand All @@ -275,7 +280,9 @@ def register_new_lead(form_entry=None):
ac_academy = ActiveCampaignAcademy.objects.filter(academy__slug=form_entry["location"]).first()

if ac_academy is None:
raise RetryTask(f"No academy found with slug {form_entry['location']}")
raise RetryTask(
f"No CRM vendor information for academy with slug {form_entry['location']}. Is Active Campaign or Brevo used?"
)

automations = get_lead_automations(ac_academy, form_entry)

Expand All @@ -285,13 +292,26 @@ def register_new_lead(form_entry=None):
else:
logger.info("automations not found")

tags = get_lead_tags(ac_academy, form_entry)
logger.info("found tags")
logger.info(set(t.slug for t in tags))
# Tags are only for ACTIVE CAMPAIGN
tags = []
if ac_academy.crm_vendor == "BREVO":
# brevo uses slugs instead of ID for automations
automations = automations.values_list("slug", flat=True)
if "tags" in form_entry and len(form_entry["tags"]) > 0:
raise Exception("Brevo CRM does not support tags, please remove them from the contact payload")
else:
if hasattr(automations, "values_list"):
automations = automations.values_list("acp_id", flat=True)

tags = get_lead_tags(ac_academy, form_entry)
logger.info("found tags")
logger.info(set(t.slug for t in tags))

if (automations is None or len(automations) == 0) and len(tags) > 0:
if tags[0].automation is None:
raise ValidationException("No automation was specified and the the specified tag has no automation either")
raise ValidationException(
"No automation was specified and the specified tag (if any) has no automation either"
)

automations = [tags[0].automation.acp_id]

Expand Down Expand Up @@ -326,30 +346,33 @@ def register_new_lead(form_entry=None):
"phone": form_entry["phone"],
}

contact = set_optional(contact, "utm_url", form_entry)
contact = set_optional(contact, "utm_location", form_entry, "location")
contact = set_optional(contact, "course", form_entry)
contact = set_optional(contact, "utm_language", form_entry, "language")
contact = set_optional(contact, "utm_country", form_entry, "country")
contact = set_optional(contact, "utm_campaign", form_entry)
contact = set_optional(contact, "utm_source", form_entry)
contact = set_optional(contact, "utm_content", form_entry)
contact = set_optional(contact, "utm_medium", form_entry)
contact = set_optional(contact, "utm_plan", form_entry)
contact = set_optional(contact, "utm_placement", form_entry)
contact = set_optional(contact, "utm_term", form_entry)
contact = set_optional(contact, "gender", form_entry, "sex")
contact = set_optional(contact, "client_comments", form_entry)
contact = set_optional(contact, "gclid", form_entry)
contact = set_optional(contact, "current_download", form_entry)
contact = set_optional(contact, "referral_key", form_entry)
contact = set_optional(contact, "utm_url", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_location", form_entry, "location", crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "course", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_language", form_entry, "language", crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_country", form_entry, "country", crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_campaign", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_source", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_content", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_medium", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_plan", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_placement", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "utm_term", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "gender", form_entry, "sex", crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "client_comments", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "gclid", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "current_download", form_entry, crm_vendor=ac_academy.crm_vendor)
contact = set_optional(contact, "referral_key", form_entry, crm_vendor=ac_academy.crm_vendor)

# only for brevo
if ac_academy.crm_vendor == "BREVO":
contact = set_optional(contact, "utm_landing", form_entry, crm_vendor=ac_academy.crm_vendor)

entry = FormEntry.objects.filter(id=form_entry["id"]).first()

if not entry:
raise ValidationException("FormEntry not found (id: " + str(form_entry["id"]) + ")")

if "contact-us" == tags[0].slug:
if len(tags) > 0 and "contact-us" == tags[0].slug:

obj = {}
if ac_academy.academy:
Expand All @@ -370,60 +393,91 @@ def register_new_lead(form_entry=None):
)

is_duplicate = entry.is_duplicate(form_entry)
# ENV Variable to fake lead storage
if is_duplicate:
entry.storage_status = "DUPLICATED"
entry.save()
logger.info("FormEntry is considered a duplicate, not sent to CRM and no automations or tags added")
return entry

# ENV Variable to fake lead storage
if get_save_leads() == "FALSE":
entry.storage_status_text = "Saved but not send to AC because SAVE_LEADS is FALSE"
entry.storage_status_text = "Saved but not send to CRM because SAVE_LEADS is FALSE"
entry.storage_status = "PERSISTED" if not is_duplicate else "DUPLICATED"
entry.save()
return entry

logger.info("ready to send contact with following details: " + str(contact))
if ac_academy.crm_vendor == "ACTIVE_CAMPAIGN":
entry = send_to_active_campaign(entry, ac_academy, contact, automations, tags)
elif ac_academy.crm_vendor == "BREVO":
entry = send_to_brevo(entry, ac_academy, contact, automations)

if entry.storage_status in ["ERROR"]:
return entry

entry.storage_status = "PERSISTED"
entry.save()

form_entry["storage_status"] = "PERSISTED"

return entry


def send_to_active_campaign(form_entry, ac_academy, contact, automations, tags):

old_client = ACOldClient(ac_academy.ac_url, ac_academy.ac_key)
response = old_client.contacts.create_contact(contact)
contact_id = response["subscriber_id"]

# save contact_id from active campaign
entry.ac_contact_id = contact_id
entry.save()
form_entry.ac_contact_id = contact_id
form_entry.save()

if "subscriber_id" not in response:
logger.error("error adding contact", response)
entry.storage_status = "ERROR"
entry.storage_status_text = "Could not save contact in CRM: Subscriber_id not found"
entry.save()

if is_duplicate:
entry.storage_status = "DUPLICATED"
entry.save()
logger.info("FormEntry is considered a duplicate, no automations or tags added")
return entry
form_entry.storage_status = "ERROR"
form_entry.storage_status_text = "Could not save contact in CRM: Subscriber_id not found"
form_entry.save()
return form_entry

client = ActiveCampaignClient(ac_academy.ac_url, ac_academy.ac_key)
if automations and not is_duplicate:
if automations:
for automation_id in automations:
data = {"contactAutomation": {"contact": contact_id, "automation": automation_id}}
response = client.contacts.add_a_contact_to_an_automation(data)

if "contacts" not in response:
logger.error(f"error triggering automation with id {str(automation_id)}", response)
raise APIException("Could not add contact to Automation")
logger.info(f"Triggered automation with id {str(automation_id)} " + str(response))
logger.debug(f"Triggered automation with id {str(automation_id)} " + str(response))

logger.info("automations was executed successfully")

if tags and not is_duplicate:
if tags:
for t in tags:
data = {"contactTag": {"contact": contact_id, "tag": t.acp_id}}
response = client.contacts.add_a_tag_to_contact(data)
logger.info("contact was tagged successfully")

entry.storage_status = "PERSISTED"
entry.save()
return form_entry

form_entry["storage_status"] = "PERSISTED"

return entry
def send_to_brevo(form_entry, ac_academy, contact, automations):

if automations.count() > 1:
raise Exception("Only one automation at a time is allowed for Brevo")

_a = automations.first()

brevo_client = Brevo(ac_academy.ac_key)
response = brevo_client.create_contact(contact, _a)

# Brevo does not answer with the contact ID when the create_contact
# is being made thru triggering a brevo event
if response and "id" in response:
form_entry.ac_contact_id = response["id"]
form_entry.save()

return form_entry


def test_ac_connection(ac_academy):
Expand Down Expand Up @@ -487,6 +541,9 @@ def update_deal_custom_fields(formentry_id: int):

def sync_tags(ac_academy):

if ac_academy.crm_vendor == "BREVO":
raise Exception("Sync method has not been implemented for Brevo Tags")

client = ActiveCampaignClient(ac_academy.ac_url, ac_academy.ac_key)
response = client.tags.list_all_tags(limit=100)

Expand Down Expand Up @@ -518,6 +575,9 @@ def sync_tags(ac_academy):

def sync_automations(ac_academy):

if ac_academy.crm_vendor == "BREVO":
raise Exception("Sync method has not been implemented for Brevo Automations")

client = ActiveCampaignClient(ac_academy.ac_url, ac_academy.ac_key)
response = client.automations.list_all_automations(limit=100)

Expand Down
Loading
Loading