diff options
Diffstat (limited to 'django/factwise-python')
25 files changed, 745 insertions, 0 deletions
diff --git a/django/factwise-python/.gitignore b/django/factwise-python/.gitignore new file mode 100644 index 0000000..6b24a29 --- /dev/null +++ b/django/factwise-python/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +env/ +venv/ +.idea/ +.vscode/ +db/ +out/ +*.sqlite3 diff --git a/django/factwise-python/ProblemStatement.md b/django/factwise-python/ProblemStatement.md new file mode 100644 index 0000000..a996b5b --- /dev/null +++ b/django/factwise-python/ProblemStatement.md @@ -0,0 +1,38 @@ +## Overview + +### Application +Implement a team project planner tool. The tool consists of API's for +* Managing users +* Manging teams +* Managing a team board and tasks within a board + + +The directory consists of base abstract classes. The goal is to implement the API methods defined in these classes +Create a module for concrete implementation of these base classes extending the base classes. +* The input and output will be JSON strings. Structure of which is mentioned in the method doc string. +* Every API needs to adhere to the constraints and raise exceptions for invalid inputs. +* The method doc, will include some additional requirements specific to the method. + +### Persistence +The application should use the local file storage for persistence. +The **db** folder should contain all the files created to persist the application data. +The choice of the file format and data type is up to the developer. +The user of the application should not be exposed to the internal file storage and only interact using the API's. + +### Submission +* Update the **README.md** file with a brief summary of your project. Include your thought process of making the choices you made. +* You are free to use any python library. Add the required dependency to **requirements.txt** +* Create a zip of the final project. Please **Do Not** include the db files* or any imported libraries. +* For non explicilty mentied requirements you are free to make assumptions and add the rationale for the assumption in the **README.md** + +### Evaluation +* Implementation of use cases +* Clean modular code +* Clear Abstractions +* Runtime Performance +* Handling edge cases +* Documentation +* Quality of Readme.md (Concise and to the point) +* Creativity and simplicity + + diff --git a/django/factwise-python/README.md b/django/factwise-python/README.md new file mode 100644 index 0000000..ce6e39c --- /dev/null +++ b/django/factwise-python/README.md @@ -0,0 +1,3 @@ +# Factwise Assestment + +Given by Anand diff --git a/django/factwise-python/__init__.py b/django/factwise-python/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/django/factwise-python/__init__.py diff --git a/django/factwise-python/factwise_submission/factwise_submission/__init__.py b/django/factwise-python/factwise_submission/factwise_submission/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/django/factwise-python/factwise_submission/factwise_submission/__init__.py diff --git a/django/factwise-python/factwise_submission/factwise_submission/asgi.py b/django/factwise-python/factwise_submission/factwise_submission/asgi.py new file mode 100644 index 0000000..3e28ed7 --- /dev/null +++ b/django/factwise-python/factwise_submission/factwise_submission/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for factwise_submission project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'factwise_submission.settings') + +application = get_asgi_application() diff --git a/django/factwise-python/factwise_submission/factwise_submission/settings.py b/django/factwise-python/factwise_submission/factwise_submission/settings.py new file mode 100644 index 0000000..c7eed22 --- /dev/null +++ b/django/factwise-python/factwise_submission/factwise_submission/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for factwise_submission project. + +Generated by 'django-admin startproject' using Django 5.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-xo20ykmr3edhn+_!(x(%81_j@3^r0!!cn*@3#%-#o@k%1*u&6+' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'plannerapp', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'factwise_submission.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'factwise_submission.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/django/factwise-python/factwise_submission/factwise_submission/urls.py b/django/factwise-python/factwise_submission/factwise_submission/urls.py new file mode 100644 index 0000000..8561be9 --- /dev/null +++ b/django/factwise-python/factwise_submission/factwise_submission/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for factwise_submission project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/django/factwise-python/factwise_submission/factwise_submission/wsgi.py b/django/factwise-python/factwise_submission/factwise_submission/wsgi.py new file mode 100644 index 0000000..58aa18b --- /dev/null +++ b/django/factwise-python/factwise_submission/factwise_submission/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for factwise_submission project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'factwise_submission.settings') + +application = get_wsgi_application() diff --git a/django/factwise-python/factwise_submission/manage.py b/django/factwise-python/factwise_submission/manage.py new file mode 100755 index 0000000..81e672d --- /dev/null +++ b/django/factwise-python/factwise_submission/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'factwise_submission.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/django/factwise-python/factwise_submission/plannerapp/__init__.py b/django/factwise-python/factwise_submission/plannerapp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/__init__.py diff --git a/django/factwise-python/factwise_submission/plannerapp/admin.py b/django/factwise-python/factwise_submission/plannerapp/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django/factwise-python/factwise_submission/plannerapp/apps.py b/django/factwise-python/factwise_submission/plannerapp/apps.py new file mode 100644 index 0000000..2eaa95f --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PlannerappConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'plannerapp' diff --git a/django/factwise-python/factwise_submission/plannerapp/base/project_board_base.py b/django/factwise-python/factwise_submission/plannerapp/base/project_board_base.py new file mode 100644 index 0000000..71262f3 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/base/project_board_base.py @@ -0,0 +1,105 @@ +class ProjectBoardBase: + """ + A project board is a unit of delivery for a project. Each board will have a set of tasks assigned to a user. + """ + + # create a board + def create_board(self, request: str): + """ + :param request: A json string with the board details. + { + "name" : "<board_name>", + "description" : "<description>", + "team_id" : "<team id>" + "creation_time" : "<date:time when board was created>" + } + :return: A json string with the response {"id" : "<board_id>"} + + Constraint: + * board name must be unique for a team + * board name can be max 64 characters + * description can be max 128 characters + """ + pass + + # close a board + def close_board(self, request: str) -> str: + """ + :param request: A json string with the user details + { + "id" : "<board_id>" + } + + :return: + + Constraint: + * Set the board status to CLOSED and record the end_time date:time + * You can only close boards with all tasks marked as COMPLETE + """ + pass + + # add task to board + def add_task(self, request: str) -> str: + """ + :param request: A json string with the task details. Task is assigned to a user_id who works on the task + { + "title" : "<board_name>", + "description" : "<description>", + "user_id" : "<team id>" + "creation_time" : "<date:time when task was created>" + } + :return: A json string with the response {"id" : "<task_id>"} + + Constraint: + * task title must be unique for a board + * title name can be max 64 characters + * description can be max 128 characters + + Constraints: + * Can only add task to an OPEN board + """ + pass + + # update the status of a task + def update_task_status(self, request: str): + """ + :param request: A json string with the user details + { + "id" : "<task_id>", + "status" : "OPEN | IN_PROGRESS | COMPLETE" + } + """ + pass + + # list all open boards for a team + def list_boards(self, request: str) -> str: + """ + :param request: A json string with the team identifier + { + "id" : "<team_id>" + } + + :return: + [ + { + "id" : "<board_id>", + "name" : "<board_name>" + } + ] + """ + pass + + def export_board(self, request: str) -> str: + """ + Export a board in the out folder. The output will be a txt file. + We want you to be creative. Output a presentable view of the board and its tasks with the available data. + :param request: + { + "id" : "<board_id>" + } + :return: + { + "out_file" : "<name of the file created>" + } + """ + pass diff --git a/django/factwise-python/factwise_submission/plannerapp/base/team_base.py b/django/factwise-python/factwise_submission/plannerapp/base/team_base.py new file mode 100644 index 0000000..29b1a5d --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/base/team_base.py @@ -0,0 +1,133 @@ +class TeamBase: + """ + Base interface implementation for API's to manage teams. + For simplicity a single team manages a single project. And there is a separate team per project. + Users can be + """ + + # create a team + def create_team(self, request: str) -> str: + """ + :param request: A json string with the team details + { + "name" : "<team_name>", + "description" : "<some description>", + "admin": "<id of a user>" + } + :return: A json string with the response {"id" : "<team_id>"} + + Constraint: + * Team name must be unique + * Name can be max 64 characters + * Description can be max 128 characters + """ + pass + + # list all teams + def list_teams(self) -> str: + """ + :return: A json list with the response. + [ + { + "name" : "<team_name>", + "description" : "<some description>", + "creation_time" : "<some date:time format>", + "admin": "<id of a user>" + } + ] + """ + pass + + # describe team + def describe_team(self, request: str) -> str: + """ + :param request: A json string with the team details + { + "id" : "<team_id>" + } + + :return: A json string with the response + + { + "name" : "<team_name>", + "description" : "<some description>", + "creation_time" : "<some date:time format>", + "admin": "<id of a user>" + } + + """ + pass + + # update team + def update_team(self, request: str) -> str: + """ + :param request: A json string with the team details + { + "id" : "<team_id>", + "team" : { + "name" : "<team_name>", + "description" : "<team_description>", + "admin": "<id of a user>" + } + } + + :return: + + Constraint: + * Team name must be unique + * Name can be max 64 characters + * Description can be max 128 characters + """ + pass + + # add users to team + def add_users_to_team(self, request: str): + """ + :param request: A json string with the team details + { + "id" : "<team_id>", + "users" : ["user_id 1", "user_id2"] + } + + :return: + + Constraint: + * Cap the max users that can be added to 50 + """ + pass + + # add users to team + def remove_users_from_team(self, request: str): + """ + :param request: A json string with the team details + { + "id" : "<team_id>", + "users" : ["user_id 1", "user_id2"] + } + + :return: + + Constraint: + * Cap the max users that can be added to 50 + """ + pass + + # list users of a team + def list_team_users(self, request: str): + """ + :param request: A json string with the team identifier + { + "id" : "<team_id>" + } + + :return: + [ + { + "id" : "<user_id>", + "name" : "<user_name>", + "display_name" : "<display name>" + } + ] + """ + pass + diff --git a/django/factwise-python/factwise_submission/plannerapp/base/user_base.py b/django/factwise-python/factwise_submission/plannerapp/base/user_base.py new file mode 100644 index 0000000..ec4dbc7 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/base/user_base.py @@ -0,0 +1,94 @@ +class UserBase: + """ + Base interface implementation for API's to manage users. + """ + + # create a user + def create_user(self, request: str) -> str: + """ + :param request: A json string with the user details + { + "name" : "<user_name>", + "display_name" : "<display name>" + } + :return: A json string with the response {"id" : "<user_id>"} + + Constraint: + * user name must be unique + * name can be max 64 characters + * display name can be max 64 characters + """ + pass + + # list all users + def list_users(self) -> str: + """ + :return: A json list with the response + [ + { + "name" : "<user_name>", + "display_name" : "<display name>", + "creation_time" : "<some date:time format>" + } + ] + """ + pass + + # describe user + def describe_user(self, request: str) -> str: + """ + :param request: A json string with the user details + { + "id" : "<user_id>" + } + + :return: A json string with the response + + { + "name" : "<user_name>", + "description" : "<some description>", + "creation_time" : "<some date:time format>" + } + + """ + pass + + # update user + def update_user(self, request: str) -> str: + """ + :param request: A json string with the user details + { + "id" : "<user_id>", + "user" : { + "name" : "<user_name>", + "display_name" : "<display name>" + } + } + + :return: + + Constraint: + * user name cannot be updated + * name can be max 64 characters + * display name can be max 128 characters + """ + pass + + def get_user_teams(self, request: str) -> str: + """ + :param request: + { + "id" : "<user_id>" + } + + :return: A json list with the response. + [ + { + "name" : "<team_name>", + "description" : "<some description>", + "creation_time" : "<some date:time format>" + } + ] + """ + pass + diff --git a/django/factwise-python/factwise_submission/plannerapp/exceptions.py b/django/factwise-python/factwise_submission/plannerapp/exceptions.py new file mode 100644 index 0000000..7be61ca --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/exceptions.py @@ -0,0 +1,9 @@ +class ValidationError(ValueError): + pass + +class NotFoundError(KeyError): + pass + +class ConflictError(Exception): + pass + diff --git a/django/factwise-python/factwise_submission/plannerapp/migrations/__init__.py b/django/factwise-python/factwise_submission/plannerapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/migrations/__init__.py diff --git a/django/factwise-python/factwise_submission/plannerapp/models.py b/django/factwise-python/factwise_submission/plannerapp/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django/factwise-python/factwise_submission/plannerapp/services.py b/django/factwise-python/factwise_submission/plannerapp/services.py new file mode 100644 index 0000000..fb8dbdf --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/services.py @@ -0,0 +1,88 @@ +# planner/services.py +import json +from .base import user_base, team_base, project_board_base +from .storage import FileStorage +from .exceptions import ValidationError, NotFoundError, ConflictError +from .utils import now_iso, new_id + +MAX_NAME = 64 +MAX_DISPLAY = 128 + +USERS = FileStorage("users.json") +TEAMS = FileStorage("teams.json") + + +class UserService(user_base): + def create_user(self, request: str) -> str: + payload = json.loads(request) + name = payload.get("name", "").strip() + display_name = payload.get("display_name", "").strip() + + if not name: + raise ValidationError("User name required") + if len(name) > MAX_NAME: + raise ValidationError("User name too long") + if len(display_name) > MAX_NAME: + raise ValidationError("Display name too long") + + users = USERS.read_all() + if any(u["name"] == name for u in users): + raise ConflictError("User name must be unique") + + user = { + "id": new_id(), + "name": name, + "display_name": display_name, + "creation_time": now_iso() + } + USERS.append(user) + return json.dumps({"id": user["id"]}) + + def list_users(self) -> str: + users = USERS.read_all() + return json.dumps([{ + "name": u["name"], + "display_name": u["display_name"], + "creation_time": u["creation_time"] + } for u in users]) + + def describe_user(self, request: str) -> str: + user_id = json.loads(request)["id"] + users = USERS.read_all() + for u in users: + if u["id"] == user_id: + return json.dumps(u) + raise NotFoundError("User not found") + + def update_user(self, request: str) -> str: + payload = json.loads(request) + user_id = payload["id"] + updates = payload.get("user", {}) + + if "name" in updates: + raise ValidationError("User name cannot be updated") + if "display_name" in updates and len(updates["display_name"]) > MAX_DISPLAY: + raise ValidationError("Display name too long") + + users = USERS.read_all() + for i, u in enumerate(users): + if u["id"] == user_id: + u.update(updates) + users[i] = u + USERS.replace(users) + return json.dumps({}) + raise NotFoundError("User not found") + + def get_user_teams(self, request: str) -> str: + user_id = json.loads(request)["id"] + teams = TEAMS.read_all() + result = [] + for t in teams: + if user_id == t.get("admin") or user_id in t.get("users", []): + result.append({ + "name": t["name"], + "description": t.get("description"), + "creation_time": t.get("creation_time") + }) + return json.dumps(result) + diff --git a/django/factwise-python/factwise_submission/plannerapp/storage.py b/django/factwise-python/factwise_submission/plannerapp/storage.py new file mode 100644 index 0000000..324e0c0 --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/storage.py @@ -0,0 +1,36 @@ +import json +import os +from filelock import FileLock + +DB_DIR = os.path.join(os.path.dirname(__file__), '..', 'db') +os.makedirs(DB_DIR, exist_ok=True) + +class FileStorage: + def __init__(self, filename): + self.path = os.path.join(DB_DIR, filename) + self.lock = FileLock(self.path + ".lock") + + def _ensure(self): + if not os.path.exists(self.path): + with open(self.path, "w") as f: + json.dump([], f) + + def read_all(self): + self._ensure() + with self.lock: + with open(self.path, "r") as f: + return json.load(f) + + def write_all(self, data): + with self.lock: + with open(self.path, "w") as f: + json.dump(data, f, indent=2) + + def append(self, item): + data = self.read_all() + data.append(item) + self.write_all(data) + + def replace(self, data): + self.write_all(data) + diff --git a/django/factwise-python/factwise_submission/plannerapp/tests.py b/django/factwise-python/factwise_submission/plannerapp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/factwise-python/factwise_submission/plannerapp/utils.py b/django/factwise-python/factwise_submission/plannerapp/utils.py new file mode 100644 index 0000000..af1d1cf --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/utils.py @@ -0,0 +1,9 @@ +import uuid +from datetime import datetime + +def now_iso(): + return datetime.utcnow().isoformat() + "Z" + +def new_id(): + return str(uuid.uuid4())[:8] + diff --git a/django/factwise-python/factwise_submission/plannerapp/views.py b/django/factwise-python/factwise_submission/plannerapp/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/django/factwise-python/factwise_submission/plannerapp/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/django/factwise-python/requirements.txt b/django/factwise-python/requirements.txt new file mode 100644 index 0000000..cad54b9 --- /dev/null +++ b/django/factwise-python/requirements.txt @@ -0,0 +1,4 @@ +#Add the project dependencies to this file +django +ipython +filelock |
