Pushed Edit Profile GUI logic into core. Cut ProfileController's role in this, and setup an isolated update_profile function to pass saves to the model

This commit is contained in:
SimplifiedPrivacy 2026-07-17 22:33:00 -04:00
parent 5235f168a3
commit 00d6fc7738
4 changed files with 158 additions and 14 deletions

View file

@ -35,13 +35,6 @@ class ProfileController:
if profile_observer is not None: if profile_observer is not None:
profile_observer.notify('created', profile) profile_observer.notify('created', profile)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile.save()
if profile_observer is not None:
profile_observer.notify('updated', profile)
@staticmethod @staticmethod
def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None): def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None):

View file

@ -0,0 +1,117 @@
# utils
from core.errors.logger import logger
from core.models.Result import Result, ResultError
# JSON Models
from core.models.BaseProfile import BaseProfile as Profile
from core.models.Subscription import Subscription
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
# ORM Models
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.orm_calls.location_calls import get_profile_location_data
SYSTEMWIDE_CHOICES = ['wireguard', 'hysteria2', 'vless']
SESSION_CHOICES = ['wireguard', 'tor', 'proxy']
"""
Steps:
1) Looks up the profile by id number
2) Filters what values to save, using the ORM for some such as location
3) Saves it with the abstract class' json serialization methods, which use the ORM methods
Returns:
Result Object
Called by:
GUI's editor_page
Raises Errors:
False
"""
def update_profile(
profile_id: int,
key: str,
new_value: str) -> Result:
final_data = None
logger.info(f"[UPDATE PROFILE] Recieved profile_id {profile_id}, key {key}, and new_value {new_value}.")
if not key or not new_value:
return Result(valid=False, data=f"Invalid Inputs of {key} and {new_value}", error_type=ResultError.INVALID_INPUT)
profile = Profile.find_by_id(profile_id)
if not profile:
error_msg = f"Invalid profile id of {profile_id}"
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# logger.info(f"[UPDATE PROFILE] Found a relevant profile {profile_id}.")
if key == 'dimentions':
profile.resolution = new_value
elif key == 'name':
profile.name = new_value
# ============= CONNECTION TYPE =============
elif key == 'connection':
if new_value == 'tor':
profile.connection.code = new_value
profile.connection.masked = True
elif new_value == 'just proxy':
profile.connection.code = 'system'
profile.connection.masked = True
else:
error_msg = 'System wide profiles not supported atm'
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# ============= BROWSER =============
elif key == 'browser':
browser_type, browser_version = new_value.split(':', 1)
profile.application_version.application_code = browser_type
profile.application_version.version_number = browser_version
elif key == 'protocol':
# ============= SYSTEMWIDE =============
if profile.connection == 'system-wide':
if new_value in SYSTEMWIDE_CHOICES:
profile.connection.code = new_value
final_data = "edit_session"
else:
error_msg = f"{new_value} is not a systemwide choice"
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# ============= SESSION =============
else:
if new_value in SESSION_CHOICES:
profile.connection.code = new_value
if new_value == 'wireguard':
profile.connection.masked = False
# ============= LOCATION =============
elif key == "location":
country_code, city_code = new_value.split("_")
location = get_profile_location_data( # SQLAlchemy
country_code=country_code,
city_code=city_code
)
# SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc.
profile.location = location
# Subscription gets wiped on a location change, and is outside ORM
profile.subscription = None
else:
return Result(valid=False, message="Invalid Value to Edit", error_type=ResultError.INVALID_INPUT)
logger.info("[UPDATE PROFILE] Passing to Profile model to save..")
try:
profile.save()
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
return Result(valid=True, data=final_data)
except:
error_msg = "Profile save failed"
return Result(valid=False, message=error_msg, error_type=ResultError.UNKNOWN)

View file

@ -41,14 +41,17 @@ def execute_location_sql(country_code: str, city_code: str, session: Session) ->
def get_profile_location_data(country_code: str, city_code: str) -> Location: def get_profile_location_data(country_code: str, city_code: str) -> Location:
# with get_session() as session: # with get_session() as session:
location_object = execute_location_sql(country_code, city_code) api_reply_object = execute_location_sql(country_code, city_code)
if location_object.valid: if api_reply_object.valid:
return location_object.data data = api_reply_object.data
location_obj = api_reply_object.data
return location_obj
else: else:
logger.error(f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}") logger.error(f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}")
return None return None
@dataclass_json @dataclass_json
@dataclass @dataclass
class BaseProfile(ABC): class BaseProfile(ABC):
@ -88,8 +91,10 @@ class BaseProfile(ABC):
def is_system_profile(self): def is_system_profile(self):
return type(self).__name__ == 'SystemProfile' return type(self).__name__ == 'SystemProfile'
def save(self: Self): def save(self: Self):
config_dict = self.to_dict() # Get dict, not JSON string config_dict = self.to_dict()
location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate. location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
if self.location: if self.location:
@ -224,10 +229,10 @@ class BaseProfile(ABC):
city_code = profile.location.code city_code = profile.location.code
# =========== GET DATA USING THAT COUNTRY & LOCATION =========== # =========== GET DATA USING THAT COUNTRY & LOCATION ===========
location_object = get_profile_location_data(country_code, city_code) location_dict = get_profile_location_data(country_code, city_code)
if location_object: if location_dict:
profile['location'] = location_object profile['location'] = location_dict
# this needs error handling if there's no location or malconformed config. # this needs error handling if there's no location or malconformed config.

29
core/models/Result.py Normal file
View file

@ -0,0 +1,29 @@
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Any
class ResultError(Enum):
"""Classified error categories."""
SUCCESS = "success"
NOT_SUPPORTED = "NOT_SUPPORTED"
INVALID_INPUT = "invalid_input"
CONNECTION = "connection"
DATABASE = "database"
UNKNOWN = "unknown"
@dataclass
class Result:
valid: bool
error_type: ResultError = ResultError.SUCCESS
data: Optional[Any] = None
message: Optional[str] = None
def user_message(self) -> str:
"""Human-readable error for the UI."""
messages = {
DBErrorType.SUCCESS: "Operation completed successfully.",
DBErrorType.UNKNOWN: f"Error: {self.message}",
}
return messages.get(self.error_type, "Unknown error")