from django.contrib.auth.models import AbstractUser
import os
import io
from django.core.files.storage import default_storage
from django.conf import settings
from django.db import models
from PIL import Image, ImageOps
from django.core.files.uploadedfile import InMemoryUploadedFile

from taggit.managers import TaggableManager

from api.models.category import Category
from common.utils import PathAndRename


class User(AbstractUser):
    """
    A new custom User model for any functionality needed in the future. Extending AbstractUser
    allows for adding new fields to the user model as needed.
    """

    class Meta:
        db_table = "users"


# Image manipulation constants
PROFILE_IMG_SIZE = (171, 171)
PROFILE_IMG_THUMB_SIZE = (40, 40)
WHITE_BG = (255, 255, 255)


class ProfileManager(models.Manager):
    def summarize(self, profile):
        from api.models.civi import Civi

        data = {
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "about_me": profile.about_me,
            "history": [
                Civi.objects.serialize(c)
                for c in Civi.objects.filter(author_id=profile.id).order_by("-created")
            ],
            "profile_image": profile.profile_image_url,
            "followers": self.followers(profile),
            "following": self.following(profile),
        }
        return data

    def chip_summarize(self, profile):
        data = {
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "profile_image": profile.profile_image_url,
        }
        return data

    def card_summarize(self, profile, request_profile):
        # Length at which to truncate 'about me' text
        about_me_truncate_length = 150

        # If 'about me' text is longer than 150 characters... add elipsis (truncate)
        ellipsis_if_too_long = (
            "" if len(profile.about_me) <= about_me_truncate_length else "..."
        )

        data = {
            "id": profile.user.id,
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "about_me": profile.about_me[:about_me_truncate_length] + ellipsis_if_too_long,
            "profile_image": profile.profile_image_url,
            "follow_state": True
            if profile in request_profile.following.all()
            else False,
            "request_profile": request_profile.first_name,
        }
        return data

    def followers(self, profile):
        return [self.chip_summarize(follower) for follower in profile.followers.all()]

    def following(self, profile):
        return [self.chip_summarize(following) for following in profile.following.all()]


profile_upload_path = PathAndRename("")


class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=63, blank=False)
    last_name = models.CharField(max_length=63, blank=False)
    about_me = models.CharField(max_length=511, blank=True)

    categories = models.ManyToManyField(
        Category, related_name="user_categories", symmetrical=False
    )
    tags = TaggableManager()

    followers = models.ManyToManyField(
        "self", related_name="follower", symmetrical=False
    )
    following = models.ManyToManyField(
        "self", related_name="followings", symmetrical=False
    )

    is_verified = models.BooleanField(default=False)
    full_profile = models.BooleanField(default=False)

    objects = ProfileManager()
    profile_image = models.ImageField(
        upload_to=profile_upload_path, blank=True, null=True
    )
    profile_image_thumb = models.ImageField(
        upload_to=profile_upload_path, blank=True, null=True
    )

    @property
    def full_name(self):
        """Returns the person's full name."""

        return f"{self.first_name} {self.last_name}"

    @property
    def profile_image_url(self):
        """Return placeholder profile image if user didn't upload one"""

        if self.profile_image:
            file_exists = default_storage.exists(
                os.path.join(settings.MEDIA_ROOT, self.profile_image.name)
            )
            if file_exists:
                return self.profile_image.url

        return "/static/img/no_image_md.png"

    @property
    def profile_image_thumb_url(self):
        """Return placeholder profile image if user didn't upload one"""

        if self.profile_image_thumb:
            file_exists = default_storage.exists(
                os.path.join(settings.MEDIA_ROOT, self.profile_image_thumb.name)
            )
            if file_exists:
                return self.profile_image_thumb.url

        return "/static/img/no_image_md.png"

    def __init__(self, *args, **kwargs):
        super(Profile, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        """ Image crop/resize and thumbnail creation """

        # New Profile image --
        if self.profile_image:
            self.resize_profile_image()

        self.full_profile = self.is_full_profile()

        super(Profile, self).save(*args, **kwargs)

    def resize_profile_image(self):
        """
        Resizes and crops the user uploaded image and creates a thumbnail version of it
        """
        profile_image_field = self.profile_image
        image_file = io.StringIO(profile_image_field.read())
        profile_image = Image.open(image_file)
        profile_image.load()

        # Resize image
        profile_image = ImageOps.fit(
            profile_image, PROFILE_IMG_SIZE, Image.ANTIALIAS, centering=(0.5, 0.5)
        )

        # Convert to JPG image format with white background
        if profile_image.mode not in ("L", "RGB"):
            white_bg_img = Image.new("RGB", PROFILE_IMG_SIZE, WHITE_BG)
            white_bg_img.paste(profile_image, mask=profile_image.split()[3])
            profile_image = white_bg_img

        # Save new cropped image
        tmp_image_file = io.StringIO()
        profile_image.save(tmp_image_file, "JPEG", quality=90)
        tmp_image_file.seek(0)
        self.profile_image = InMemoryUploadedFile(
            tmp_image_file,
            "ImageField",
            self.profile_image.name,
            "image/jpeg",
            tmp_image_file.len,
            None,
        )
        # Make a Thumbnail Image for the new resized image
        thumb_image = profile_image.copy()
        thumb_image.thumbnail(PROFILE_IMG_THUMB_SIZE, resample=Image.ANTIALIAS)
        tmp_image_file = io.StringIO()
        thumb_image.save(tmp_image_file, "JPEG", quality=90)
        tmp_image_file.seek(0)
        self.profile_image_thumb = InMemoryUploadedFile(
            tmp_image_file,
            "ImageField",
            self.profile_image.name,
            "image/jpeg",
            tmp_image_file.len,
            None,
        )

    def is_full_profile(self):
        if self.first_name and self.last_name:
            return True
        else:
            return False

project/accounts/models.py
from django.contrib.auth.models import AbstractUser
import os
import io
from django.core.files.storage import default_storage
from django.conf import settings
from django.db import models
from PIL import Image, ImageOps
from django.core.files.uploadedfile import InMemoryUploadedFile

from taggit.managers import TaggableManager

from api.models import Category
from common.utils import PathAndRename


class User(AbstractUser):
    """
    A new custom User model for any functionality needed in the future. Extending AbstractUser
    allows for adding new fields to the user model as needed.
    """

    class Meta:
        db_table = "users"


# Image manipulation constants
PROFILE_IMG_SIZE = (171, 171)
PROFILE_IMG_THUMB_SIZE = (40, 40)
WHITE_BG = (255, 255, 255)


class ProfileManager(models.Manager):
    def summarize(self, profile):
        from api.models.civi import Civi

        data = {
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "about_me": profile.about_me,
            "history": [
                Civi.objects.serialize(c)
                for c in Civi.objects.filter(author_id=profile.id).order_by("-created")
            ],
            "profile_image": profile.profile_image_url,
            "followers": self.followers(profile),
            "following": self.following(profile),
        }
        return data

    def chip_summarize(self, profile):
        data = {
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "profile_image": profile.profile_image_url,
        }
        return data

    def card_summarize(self, profile, request_profile):
        # Length at which to truncate 'about me' text
        about_me_truncate_length = 150

        # If 'about me' text is longer than 150 characters... add elipsis (truncate)
        ellipsis_if_too_long = (
            "" if len(profile.about_me) <= about_me_truncate_length else "..."
        )

        data = {
            "id": profile.user.id,
            "username": profile.user.username,
            "first_name": profile.first_name,
            "last_name": profile.last_name,
            "about_me": profile.about_me[:about_me_truncate_length] + ellipsis_if_too_long,
            "profile_image": profile.profile_image_url,
            "follow_state": True
            if profile in request_profile.following.all()
            else False,
            "request_profile": request_profile.first_name,
        }
        return data

    def followers(self, profile):
        return [self.chip_summarize(follower) for follower in profile.followers.all()]

    def following(self, profile):
        return [self.chip_summarize(following) for following in profile.following.all()]


profile_upload_path = PathAndRename("")


class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=63, blank=False)
    last_name = models.CharField(max_length=63, blank=False)
    about_me = models.CharField(max_length=511, blank=True)

    categories = models.ManyToManyField(
        Category, related_name="user_categories", symmetrical=False
    )
    tags = TaggableManager()

    followers = models.ManyToManyField(
        "self", related_name="follower", symmetrical=False
    )
    following = models.ManyToManyField(
        "self", related_name="followings", symmetrical=False
    )

    is_verified = models.BooleanField(default=False)
    full_profile = models.BooleanField(default=False)

    objects = ProfileManager()
    profile_image = models.ImageField(
        upload_to=profile_upload_path, blank=True, null=True
    )
    profile_image_thumb = models.ImageField(
        upload_to=profile_upload_path, blank=True, null=True
    )

    @property
    def full_name(self):
        """Returns the person's full name."""

        return f"{self.first_name} {self.last_name}"

    @property
    def profile_image_url(self):
        """Return placeholder profile image if user didn't upload one"""

        if self.profile_image:
            file_exists = default_storage.exists(
                os.path.join(settings.MEDIA_ROOT, self.profile_image.name)
            )
            if file_exists:
                return self.profile_image.url

        return "/static/img/no_image_md.png"

    @property
    def profile_image_thumb_url(self):
        """Return placeholder profile image if user didn't upload one"""

        if self.profile_image_thumb:
            file_exists = default_storage.exists(
                os.path.join(settings.MEDIA_ROOT, self.profile_image_thumb.name)
            )
            if file_exists:
                return self.profile_image_thumb.url

        return "/static/img/no_image_md.png"

    def __init__(self, *args, **kwargs):
        super(Profile, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        """ Image crop/resize and thumbnail creation """

        # New Profile image --
        if self.profile_image:
            self.resize_profile_image()

        self.full_profile = self.is_full_profile()

        super(Profile, self).save(*args, **kwargs)

    def resize_profile_image(self):
        """
        Resizes and crops the user uploaded image and creates a thumbnail version of it
        """
        profile_image_field = self.profile_image
        image_file = io.StringIO(profile_image_field.read())
        profile_image = Image.open(image_file)
        profile_image.load()

        # Resize image
        profile_image = ImageOps.fit(
            profile_image, PROFILE_IMG_SIZE, Image.ANTIALIAS, centering=(0.5, 0.5)
        )

        # Convert to JPG image format with white background
        if profile_image.mode not in ("L", "RGB"):
            white_bg_img = Image.new("RGB", PROFILE_IMG_SIZE, WHITE_BG)
            white_bg_img.paste(profile_image, mask=profile_image.split()[3])
            profile_image = white_bg_img

        # Save new cropped image
        tmp_image_file = io.StringIO()
        profile_image.save(tmp_image_file, "JPEG", quality=90)
        tmp_image_file.seek(0)
        self.profile_image = InMemoryUploadedFile(
            tmp_image_file,
            "ImageField",
            self.profile_image.name,
            "image/jpeg",
            tmp_image_file.len,
            None,
        )
        # Make a Thumbnail Image for the new resized image
        thumb_image = profile_image.copy()
        thumb_image.thumbnail(PROFILE_IMG_THUMB_SIZE, resample=Image.ANTIALIAS)
        tmp_image_file = io.StringIO()
        thumb_image.save(tmp_image_file, "JPEG", quality=90)
        tmp_image_file.seek(0)
        self.profile_image_thumb = InMemoryUploadedFile(
            tmp_image_file,
            "ImageField",
            self.profile_image.name,
            "image/jpeg",
            tmp_image_file.len,
            None,
        )

    def is_full_profile(self):
        if self.first_name and self.last_name:
            return True
        else:
            return False

project/accounts/models.py
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..353944cbd
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,33 @@
+name: CI Workflow
+
+on:
+  pull_request:
+    branches : [ develop ]
+  
+  workflow_dispatch: 
+  
+  push:
+
+jobs:
+  python-tests:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v1
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.7
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements/dev.txt
+        pip install pytest-django
+    - name: Run migrations
+      run: python project/manage.py migrate
+    - name: Run tests
+      env:
+        CIVIWIKI_LOCAL_NAME : True
+        DJANGO_SETTINGS_MODULE: core.settings
+      run: |
+        cd project
+        pytest -p no:warning
diff --git a/project/accounts/models.py b/project/accounts/models.py
index 5d48149b1..9ca081b49 100644
--- a/project/accounts/models.py
+++ b/project/accounts/models.py
@@ -9,7 +9,7 @@
 
 from taggit.managers import TaggableManager
 
-from api.models.category import Category
+from api.models import Category
 from common.utils import PathAndRename
 
 
diff --git a/project/api/tests/__init__.py b/project/api/tests/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/api/tests/test_propublica.py b/project/api/tests/test_propublica.py
deleted file mode 100644
index 49124443b..000000000
--- a/project/api/tests/test_propublica.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from mock import patch, MagicMock
-
-from rest_framework.test import APITestCase, override_settings
-
-from ..propublica import ProPublicaAPI
-from .propublica_responses import PROPUBLICA_CORRECT_RESPONSE
-
-
-@override_settings(PROPUBLICA_API_KEY="test-token")
-class ProPublicaAPITestCase(APITestCase):
-    def setUp(self):
-        self.api_instance = ProPublicaAPI()
-
-    def test_headers_are_set_correctly(self):
-        self.assertDictEqual(
-            self.api_instance.auth_headers, {"X-API-Key": "test-token"}
-        )
-
-    @patch("api.propublica.requests.get")
-    def test_if_search_returns_correct_results(self, get_mock):
-        get_mock.return_value = MagicMock(
-            json=MagicMock(return_value=PROPUBLICA_CORRECT_RESPONSE)
-        )
-        query = "query"
-        data = self.api_instance.search(query)
-        self.assertEqual(data, PROPUBLICA_CORRECT_RESPONSE)
-        get_mock.assert_called_once_with(
-            self.api_instance.URL.format(query=query),
-            headers=self.api_instance.auth_headers,
-        )
diff --git a/project/test_framework/__init__.py b/project/test_framework/__init__.py
deleted file mode 100644
index 1a7e0dd46..000000000
--- a/project/test_framework/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from contextlib import contextmanager
-
-from aloe import around, world
-from selenium import webdriver
-
-"""
-this method sets a global variable of type WebDriver. This is the browser.
-@around.all is a global hook. It will be ran once before all scenarios
-After the last scenario is completed (pass or fail) the WebDriver will be closed
-"""
-
-
-@around.all
-@contextmanager
-def with_chrome():
-    options = webdriver.ChromeOptions()
-    options.add_argument("--headless")
-    options.add_argument("--disable-extensions")
-    options.add_argument("--no-sandbox")
-    world.browser = webdriver.Chrome(options=options)
-    yield
-    world.browser.quit()
-    delattr(world, "browser")
diff --git a/project/test_framework/accessability/__init__.py b/project/test_framework/accessability/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/test_framework/accessability/features/__init__.py b/project/test_framework/accessability/features/__init__.py
deleted file mode 100644
index 34278a88c..000000000
--- a/project/test_framework/accessability/features/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from aloe import step
-
-from .accessability import login, logout, is_user_loggedin
-
-
-@step(r"a user with \"(.*?)\" credentials tries to log in")
-def user_logs_in(self, correct):
-    login(correct)
-
-
-@step(r"the user is logged in")
-def user_is_logged_in(self):
-    assert is_user_loggedin()
-
-
-@step(r"the user can log out")
-def user_logs_out(self):
-    logout()
-
-
-@step(r"the user is not logged in")
-def user_not_logged_in(self):
-    assert not is_user_loggedin()
diff --git a/project/test_framework/accessability/features/accessability.feature b/project/test_framework/accessability/features/accessability.feature
deleted file mode 100644
index 1dabce843..000000000
--- a/project/test_framework/accessability/features/accessability.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: Accessibility  tests
-
-  Scenario: A user can login and logout
-    Given a user with "correct" credentials tries to log in
-    When the user is logged in
-    Then the user can log out
-
-  Scenario: Users cannot login with wrong password
-    Given a user with "incorrect" credentials tries to log in
-    Then the user is not logged in
\ No newline at end of file
diff --git a/project/test_framework/accessability/features/accessability.py b/project/test_framework/accessability/features/accessability.py
deleted file mode 100644
index 29e0faf1a..000000000
--- a/project/test_framework/accessability/features/accessability.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from test_framework.utils import *
-
-
-def login(credentials):
-    if is_user_loggedin():
-        return
-    else:
-        open_home_page()
-        el = find_element(login_signup_button)
-        el.click()
-        wait_for_page("login")
-        find_element(username_field).send_keys(username)
-        if credentials == "correct":
-            find_element(password_field).send_keys(password)
-            find_element(login_button).click()
-        elif credentials == "incorrect":
-            find_element(password_field).send_keys(password_wrong)
-
-
-def is_user_loggedin():
-    try:
-        return find_element(user_label).is_displayed()
-    except:
-        return
-
-
-def logout():
-    find_element(nav_menu).click()
-    find_element(logout_button).click()
diff --git a/project/test_framework/my_feed/__init__.py b/project/test_framework/my_feed/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/test_framework/my_feed/features/__init__.py b/project/test_framework/my_feed/features/__init__.py
deleted file mode 100644
index 9558c1dc6..000000000
--- a/project/test_framework/my_feed/features/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from test_framework.accessability.features import *
-from test_framework.accessability.features.accessability import *
diff --git a/project/test_framework/my_feed/features/my_feed.feature b/project/test_framework/my_feed/features/my_feed.feature
deleted file mode 100644
index 8bb776080..000000000
--- a/project/test_framework/my_feed/features/my_feed.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: My Feed tests
-
-  Scenario: User can view My Feed
-    Given a user with "correct" credentials tries to log in
-    When user opens the "My Feed" menu
-    And My Feed is displayed
-    And user can filter threads by category
-    And the user can "remove" the "Agriculture" category
-    And the user can "add" the "Agriculture" category
-    Then the trending issues are displayed
\ No newline at end of file
diff --git a/project/test_framework/my_profile/__init__.py b/project/test_framework/my_profile/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/test_framework/my_profile/features/__init__.py b/project/test_framework/my_profile/features/__init__.py
deleted file mode 100644
index 9558c1dc6..000000000
--- a/project/test_framework/my_profile/features/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from test_framework.accessability.features import *
-from test_framework.accessability.features.accessability import *
diff --git a/project/test_framework/my_profile/features/my_profile.feature b/project/test_framework/my_profile/features/my_profile.feature
deleted file mode 100644
index 124ae8bc7..000000000
--- a/project/test_framework/my_profile/features/my_profile.feature
+++ /dev/null
@@ -1,7 +0,0 @@
-Feature: My Profile tests
-
-  Scenario: User can change My Profile settings
-    Given a user with "correct" credentials tries to log in
-    When user opens the "My Profile" menu
-    And the user can view it's activity
-    Then the user can change About Me
\ No newline at end of file
diff --git a/project/test_framework/selectors.py b/project/test_framework/selectors.py
deleted file mode 100644
index e98403086..000000000
--- a/project/test_framework/selectors.py
+++ /dev/null
@@ -1,55 +0,0 @@
-civi_wiki_url = "https://www.civiwiki.org"
-username = "unit1test"
-password = "unit1test"
-password_wrong = "unit123test"
-
-# Accessibility
-login_signup_button = ".login-btn"
-username_field = "#username"
-password_field = "#password"
-login_button = ".login-button"
-user_label = ".label-username"
-civi_wiki_logo = ".wordmark"
-nav_menu = ".nav-menu"
-logout_button = "a.dropdown-item:nth-child(5)"
-settings_button = "a.dropdown-item:nth-child(4)"
-my_profile_button = "a.dropdown-item:nth-child(2)"
-my_feed_button = "a.dropdown-item:nth-child(1)"
-
-# Settings
-email_field = "#email"
-first_name_field = "#first_name"
-last_name_field = "#last_name"
-location_field = ".location-info"
-location_set = "#autocomplete"
-activity = ".profile-civi-list"
-edit_profile_butoon = "a.btn"
-about_me_field = "#about_me"
-my_feed_list = "#feed-list"
-manage_categories = "#manage-categories"
-choose_button = "#choose-category"
-trending_issues = ".list-title"
-
-# Threads
-add_civi_button = ".add-civi"
-add_problem_button = "button.btn-small:nth-child(1)"
-add_cause_button = "button.btn-small:nth-child(2)"
-add_solution_button = "button.btn:nth-child(3)"
-civi_title_field = "#civi-title"
-civi_description_field = "#civi-body"
-civi_links_field = "#civi_links"
-create_civi_button = ".create-new-civi"
-first_civi = ".civi-title > span:nth-child(1)"
-create_thread_button = ".new-thread"
-thread_title = "#thread-title"
-thread_summary = "#thread-body"
-thread_type_dropdown = (
-    ".new-thread-level-selection > div:nth-child(1) > div:nth-child(1)"
-)
-thread_category_dropdown = ".new-thread-category-selection > div:nth-child(1) > div:nth-child(1) > input:nth-child(2)"
-thread_create_draft = ".draft-new-thread"
-thread_publish = "#js-publish-btn"
-add_response_button = "#add-new-response"
-response_title = "#response-title"
-response_body = "#response-body"
-response_confirm = ".create-new-response"
diff --git a/project/test_framework/settings/__init__.py b/project/test_framework/settings/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/test_framework/settings/features/__init__.py b/project/test_framework/settings/features/__init__.py
deleted file mode 100644
index 988644d7f..000000000
--- a/project/test_framework/settings/features/__init__.py
+++ /dev/null
@@ -1,149 +0,0 @@
-import time
-from selenium.webdriver.common.keys import Keys
-
-from test_framework.utils import *
-from test_framework.accessability.features import *
-from test_framework.accessability.features.accessability import *
-
-
-def open_menu(menu):
-    find_element(nav_menu).click()
-    if menu == "Settings":
-        find_element(settings_button).click()
-        wait_for_page("/settings")
-    if menu == "My Profile":
-        find_element(my_profile_button).click()
-        wait_for_page(username)
-    if menu == "My Feed":
-        find_element(my_feed_button).click()
-
-
-def get_settings():
-    settings = {
-        "email": world.browser.find_element_by_css_selector(email_field).get_attribute(
-            "value"
-        ),
-        "username": world.browser.find_element_by_css_selector(
-            username_field
-        ).get_attribute("value"),
-        "first name": find_element(first_name_field).get_attribute("value"),
-        "last name": find_element(last_name_field).get_attribute("value"),
-        "location": find_element(location_field).text,
-    }
-    return settings
-
-
-def set_settings():
-    time.sleep(1)
-    first_name_1 = "unit"
-    first_name_2 = "unit2"
-    last_name_1 = "test"
-    last_name_2 = "test2"
-    location_1 = "Washington, DC, United States"
-    location_2 = "New York, NY, United States"
-
-    if find_element(first_name_field).get_attribute("value") == first_name_1:
-        find_element(first_name_field).clear()
-        find_element(first_name_field).send_keys(first_name_2)
-    elif find_element(first_name_field).get_attribute("value") == first_name_2:
-        find_element(first_name_field).clear()
-        find_element(first_name_field).send_keys(first_name_1)
-
-    if find_element(last_name_field).get_attribute("value") == last_name_1:
-        find_element(last_name_field).clear()
-        find_element(last_name_field).send_keys(last_name_2)
-    elif find_element(last_name_field).get_attribute("value") == last_name_2:
-        find_element(last_name_field).clear()
-        find_element(last_name_field).send_keys(last_name_1)
-
-    if (
-        location_1.replace(", DC, United States", "")
-        in find_element(location_field).text
-    ):
-        find_element(location_set).send_keys(location_2)
-        time.sleep(1)
-        find_element(location_set).send_keys(Keys.DOWN)
-        find_element(location_set).send_keys(Keys.RETURN)
-    elif (
-        location_2.replace(", NY, United States", "")
-        in find_element(location_field).text
-    ):
-        find_element(location_set).send_keys(location_1)
-        time.sleep(1)
-        find_element(location_set).send_keys(Keys.DOWN)
-        find_element(location_set).send_keys(Keys.RETURN)
-
-    find_element(first_name_field).click()
-
-
-def is_activity_displayed():
-    return find_element(activity).is_displayed()
-
-
-def change_about_me():
-    first_about = "this is the first about me string"
-    second_about = "this is the secod about me string"
-    find_element(edit_profile_butoon).click()
-    about_me = find_element(about_me_field)
-    world.first_about_me = about_me.get_attribute("value")
-
-    if about_me.get_attribute("value") == first_about:
-        about_me.clear()
-        about_me.send_keys(second_about)
-        about_me.send_keys(Keys.TAB)
-    elif about_me.get_attribute("value") == second_about:
-        about_me.clear()
-        about_me.send_keys(first_about)
-        about_me.send_keys(Keys.TAB)
-
-
-def get_about_me():
-    find_element(edit_profile_butoon).click()
-    return find_element(about_me_field).get_attribute("value")
-
-
-def is_my_feed_displayed():
-    return find_element(my_feed_list).is_displayed()
-
-
-def select_filter(filter):
-    categories = find_elements_by_class("category-item")
-
-    for category in categories:
-        if category.text == filter:
-            category.click()
-            break
-
-
-def get_threads_count():
-    return len(find_elements_by_class("s12"))
-
-
-def add_remove_category(mode, wanted_category):
-    find_element(manage_categories).click()
-    time.sleep(1)
-    categories = find_elements_by_class("category-checkbox")
-    for category in categories:
-        if wanted_category in category.text:
-            checkbox = category.find_element_by_class_name("filled-in")
-            if mode == "remove" and checkbox.get_attribute("checked") == "true":
-                category.find_elements_by_css_selector("*")[1].click()
-                break
-            elif mode == "add" and checkbox.get_attribute("checked") != "true":
-                category.find_elements_by_css_selector("*")[1].click()
-                break
-
-    find_element(choose_button).click()
-    time.sleep(3)
-
-
-def find_category(wanted_category):
-    categories = find_elements_by_class("category-item")
-    for category in categories:
-        if category.text == wanted_category:
-            return "true"
-    return
-
-
-def is_trending_issues():
-    return find_element(trending_issues).is_displayed()
diff --git a/project/test_framework/settings/features/settings.feature b/project/test_framework/settings/features/settings.feature
deleted file mode 100644
index e811b004a..000000000
--- a/project/test_framework/settings/features/settings.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Settings tests
-
-  Scenario: User can change it's settings
-    Given a user with "correct" credentials tries to log in
-    When user opens the "Settings" menu
-    And the current settings are saved
-    And the user enters new settings
-    Then the settings are changed
\ No newline at end of file
diff --git a/project/test_framework/threads/__init__.py b/project/test_framework/threads/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/project/test_framework/threads/features/__init__.py b/project/test_framework/threads/features/__init__.py
deleted file mode 100644
index 5951b300f..000000000
--- a/project/test_framework/threads/features/__init__.py
+++ /dev/null
@@ -1,132 +0,0 @@
-import time
-from selenium.webdriver.common.keys import Keys
-
-from test_framework.utils import *
-from test_framework.accessability.features import *
-
-
-def open_thread(thread_name):
-    time.sleep(3)
-    threads = find_elements_by_class("thread-title")
-    for thread in threads:
-        if thread.text == thread_name:
-            thread.click()
-            wait_for_page("/thread/")
-            break
-
-
-def add_civi(mode, title, description, links):
-    time.sleep(1)
-    find_element(add_civi_button).click()
-    if mode == "problem":
-        find_element(add_problem_button).click()
-        find_element(civi_title_field).send_keys(title)
-        find_element(civi_description_field).send_keys(description)
-    elif mode == "cause":
-        find_element(add_cause_button).click()
-        find_element(civi_title_field).send_keys(title)
-        find_element(civi_description_field).send_keys(description)
-        find_element(civi_links_field).send_keys(links)
-        find_element(civi_links_field).send_keys(Keys.RETURN)
-        time.sleep(1)
-    elif mode == "solution":
-        find_element(add_solution_button).click()
-        find_element(civi_title_field).send_keys(title)
-        find_element(civi_description_field).send_keys(description)
-        find_element(civi_links_field).send_keys(links)
-        find_element(civi_links_field).send_keys(Keys.RETURN)
-        time.sleep(1)
-
-    find_element(create_civi_button).click()
-    return find_element(first_civi).is_displayed()
-
-
-def create_thread(thread_type, title, summary, category):
-    find_element(create_thread_button).click()
-    find_element(thread_title).send_keys(title)
-    find_element(thread_summary).send_keys(summary)
-    find_element(thread_type_dropdown).click()
-
-    for element in find_element(thread_type_dropdown).find_elements_by_css_selector(
-        "*"
-    ):
-        if element.text == thread_type:
-            element.click()
-            break
-
-    find_element(thread_category_dropdown).click()
-    time.sleep(1)
-    find_element(thread_category_dropdown).send_keys(Keys.DOWN)
-    find_element(thread_category_dropdown).send_keys(Keys.RETURN)
-    time.sleep(1)
-    # for element in find_element_by_class("new-thread-category-selection").find_elements_by_css_selector("*"):
-    #     if element.text in category:
-    #         scroll_into_view(element)
-    #         # js_click(element)
-    #         element.click()
-    #         break
-    find_element(thread_create_draft).click()
-    time.sleep(1)
-    open_thread(title)
-    find_element(thread_publish).click()
-    time.sleep(3)
-
-
-def civi_vote(civi_title, vote):
-    for civi in find_elements_by_class("civi-card"):
-        if civi_title in civi.find_element_by_class_name("text-wrapper").text:
-            click_vote(vote, civi)
-
-
-def click_vote(vote, element):
-    options = element.find_elements_by_class_name("rating-wrapper")
-    if vote == "DISAGREE":
-        options[0].find_element_by_class_name("rating-button").click()
-    elif vote == "NEUTRAL":
-        options[2].find_element_by_class_name("rating-button").click()
-    elif vote == "AGREE":
-        options[4].find_element_by_class_name("rating-button").click()
-    time.sleep(3)
-
-
-def is_civi_visible(type):
-    if type == "cause":
-        number_of_elements = find_elements_by_class("cause-nav-indiv")
-    elif type == "solution":
-        number_of_elements = find_elements_by_class("solution-nav-indiv")
-    elif type == "problem":
-        number_of_elements = find_elements_by_class("problem-nav-indiv")
-
-    if len(number_of_elements) > 0:
-        return "true"
-    else:
-        return
-
-
-def add_comment(civi_title, response_title_text, response_body_text):
-    for civi in find_elements_by_class("civi-card"):
-        if civi_title in civi.find_element_by_class_name("text-wrapper").text:
-            # time.sleep(1)
-            civi.click()
-            # time.sleep(1)
-            try:
-                find_element(add_response_button).click()
-            except:
-                civi.click()
-                find_element(add_response_button).click()
-            find_element(response_title).send_keys(response_title_text)
-            find_element(response_body).send_keys(response_body_text)
-            find_element(response_confirm).click()
-            # time.sleep(3)
-            break
-
-
-def remove_civi(civi_title):
-    time.sleep(1)
-    for civi in find_elements_by_class("civi-card"):
-        if civi_title in civi.find_element_by_class_name("text-wrapper").text:
-            try:
-                civi.find_element_by_class_name("delete").click()
-            except:
-                time.sleep(2)
-                civi.find_element_by_class_name("delete").click()
diff --git a/project/test_framework/threads/features/threads.feature b/project/test_framework/threads/features/threads.feature
deleted file mode 100644
index c4d419939..000000000
--- a/project/test_framework/threads/features/threads.feature
+++ /dev/null
@@ -1,38 +0,0 @@
-Feature: Threads tests
-
-  Scenario: User can create threads
-    Given a user with "correct" credentials tries to log in
-    And user opens the "My Feed" menu
-    When user can create a new "Federal" thread
-    And user opens the "My Feed" menu
-    And user can create a new "State" thread
-    And user opens the "My Feed" menu
-#    Then the user can delete a thread
-
-  Scenario: User can add new problems, causes and solutions
-    Given a user with "correct" credentials tries to log in
-    And user opens the "My Feed" menu
-    And the user opens the "automation Federal thread" thread
-    When the user can add a "problem" to the thread
-    And the user can add a "cause" to the thread
-    And the user can add a "solution" to the thread
-
-  Scenario: Civis are only visible when user votes AGREE
-    Given a user with "correct" credentials tries to log in
-    When the user opens the "automation Federal thread" thread
-    Then the "cause" is "not visible" when user clicks "DISAGREE" on "this is a problem"
-    And the "cause" is "visible" when user clicks "AGREE" on "this is a problem"
-    And the "solution" is "not visible" when user clicks "DISAGREE" on "c1"
-    And the "solution" is "visible" when user clicks "AGREE" on "c1"
-
-  Scenario: User can add response to civis
-    Given a user with "correct" credentials tries to log in
-    When the user opens the "automation Federal thread" thread
-    Then the user can add response to "this is a problem"
-
-  Scenario: User can remove civis
-    Given a user with "correct" credentials tries to log in
-    When the user opens the "automation Federal thread" thread
-    Then the user can remove "solution" called "this is a test solution"
-    And the user can remove "cause" called "c1"
-    And the user can remove "problem" called "this is a problem"
\ No newline at end of file
diff --git a/project/test_framework/utils.py b/project/test_framework/utils.py
deleted file mode 100644
index 3b9171337..000000000
--- a/project/test_framework/utils.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from aloe import world
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support.wait import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
-
-from test_framework.selectors import *
-
-webDriverWaitInSeconds = 5
-
-
-def open_home_page():
-    world.browser.get(civi_wiki_url)
-    wait_for_page(civi_wiki_url)
-
-
-########################################################
-# Waits for page with URL containing "url" to load
-def wait_for_page(url):
-    wait = WebDriverWait(world.browser, webDriverWaitInSeconds)
-    wait.until(EC.url_contains(url))
-
-
-# Returns a list of WebElement found by a CSS Selector
-def find_elements(selector):
-    return world.browser.find_elements_by_css_selector(selector)
-
-
-# Returns a list of WebElement found by Class name
-def find_elements_by_class(selector):
-    return world.browser.find_elements_by_class_name(selector)
-
-
-# Returns a single WebElement found by a CSS Selector
-def find_element(selector):
-    element = WebDriverWait(world.browser, webDriverWaitInSeconds).until(
-        EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
-    )
-    return element
-
-
-# Returns a single WebElement found by Class name
-def find_element_by_class(selector):
-    return world.browser.find_element_by_class_name(selector)
-
-
-# Returns a single WebElement found by Name
-def find_element_by_name(selector):
-    element = WebDriverWait(world.browser, webDriverWaitInSeconds).until(
-        EC.element_to_be_clickable((By.NAME, selector))
-    )
-    return element
-
-
-# Scrolls "element" into view. Used when element is outside the field of view
-def scroll_into_view(element):
-    world.browser.execute_script("arguments[0].scrollIntoView();", element)
-    return element
-
-
-# Uses JS to click on an "element". Used when "element" is not a button or text field
-# Ex. homes in the Homes page
-def js_click(element):
-    world.browser.execute_script("arguments[0].click();", element)
diff --git a/project/threads/tests.py b/project/threads/tests.py
index 7ce503c2d..a39b155ac 100644
--- a/project/threads/tests.py
+++ b/project/threads/tests.py
@@ -1,3 +1 @@
-from django.test import TestCase
-
 # Create your tests here.

{FEAT}: Automated testing with actions.
### Idea summary

Usage of GitHub actions.

### Further details

We can use GitHub Actions to check/test the code that is being pushed upstream via PRs and it can be tested before merging automatically (Technically it is Continuous Integration).
