summaryrefslogtreecommitdiff
path: root/django/factwise-python
diff options
context:
space:
mode:
authoranand <anand.panchdhari@gmail.com>2025-12-13 17:06:22 +0530
committeranand <anand.panchdhari@gmail.com>2025-12-13 17:06:22 +0530
commitbd3664c6315dca15d15bdf4d4a6342b2131e041c (patch)
tree1c6e326bc935e4bd78490f7f495757198dd826c2 /django/factwise-python
Diffstat (limited to 'django/factwise-python')
-rw-r--r--django/factwise-python/.gitignore9
-rw-r--r--django/factwise-python/ProblemStatement.md38
-rw-r--r--django/factwise-python/README.md3
-rw-r--r--django/factwise-python/__init__.py0
-rw-r--r--django/factwise-python/factwise_submission/factwise_submission/__init__.py0
-rw-r--r--django/factwise-python/factwise_submission/factwise_submission/asgi.py16
-rw-r--r--django/factwise-python/factwise_submission/factwise_submission/settings.py123
-rw-r--r--django/factwise-python/factwise_submission/factwise_submission/urls.py22
-rw-r--r--django/factwise-python/factwise_submission/factwise_submission/wsgi.py16
-rwxr-xr-xdjango/factwise-python/factwise_submission/manage.py22
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/__init__.py0
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/admin.py3
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/apps.py6
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/base/project_board_base.py105
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/base/team_base.py133
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/base/user_base.py94
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/exceptions.py9
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/migrations/__init__.py0
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/models.py3
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/services.py88
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/storage.py36
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/tests.py3
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/utils.py9
-rw-r--r--django/factwise-python/factwise_submission/plannerapp/views.py3
-rw-r--r--django/factwise-python/requirements.txt4
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